自己写一个Arthas工具(简化版),功能点包括:

  • 查看内存使用情况
  • 生成堆内存快照
  • 打印栈信息
  • 打印类加载器
  • 打印类的源码
  • 打印方法执行的参数和耗时

提供一个独立的Jar,无侵入性,可用于任何Java程序:

在这里插入图片描述

0、客户端代码

获取所有的Java进程的ID,让用户只需选择PID,连接Java进程,加载Java Agent的Jar,进行动态代理:

public class AttachTest {
    public static void main(String[] args) throws Exception {
        //获取进程列表,让用户自己选择连接哪个PID
        //执行jps指令
        Process process = Runtime.getRuntime().exec("jps");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        try (bufferedReader) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        }
        //用户输入进程ID
        Scanner scanner = new Scanner(System.in);
        String pid = scanner.next();
        //连接用户输入的进程
        VirtualMachine vm = VirtualMachine.attach(pid);
        //执行Java Agent的里的agentmain方法
        vm.loadAgent("D:\\jmh2\\llg-agent\\target\\llg-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");

    }
}

在这里插入图片描述

1、JMX

JDK1.5起,提供Java Management Extensions (JMX) 技术,JMX技术使得开发者可以在内存中保存一个MXbean对象,存一些配置信息(类似对象容器的方式去存放一种特有的对象),另外,JVM也将一些程序的运行信息放入了MXbean对象。

简言之,通过JMX,写入或者读取MXbean,可实现:

  • 运行时配置的获取和更改
  • 获取应用程序的运行信息,如:线程栈、内存、类的信息

应用场景:

  • VisualVM使用JMX技术远程连接的方式,由Java程序暴露一个端口,让VisualVM拿到MXbean对象的信息,做一个内存、线程等信息的展示
  • 自定义一个JavaAgent,调用方法操作MXbean对象
    在这里插入图片描述

关于JMX能调用的方法:

ManagementFactory.getMemoryPoolMXBeans() //获取内存信息

其他方法:

在这里插入图片描述

2、实现:查看内存使用情况

调用getMemoryPoolMXBeans方法,获取JVM各块内存对象的List,分堆和非堆打印:

public class MemoryCommand {

    //打印所有的内存信息
    public static void printMemory() {
        //获取内存信息,返回List的结果,List中有伊甸园区、老年代、元空间等对象
        //下面分堆和非堆,分开打印
        List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();
        System.out.println("堆内存:");
        //堆内存
        getMemoryInfo(memoryPoolMXBeans,MemoryType.HEAP);
        System.out.println("非堆内存:");
        //非堆内存
        getMemoryInfo(memoryPoolMXBeans,MemoryType.NON_HEAP);
    }

    /**
     * 处理内存信息
     * @param memoryPoolMXBeans 内存信息
     * @param heapType 堆或非堆
     */
    public static void getMemoryInfo(List<MemoryPoolMXBean> memoryPoolMXBeans,MemoryType heapType){
        memoryPoolMXBeans.stream().filter(x -> x.getType().equals(heapType))
                .forEach(x -> {
                    StringBuilder sb = new StringBuilder();
                    sb.append("name:")
                            .append(x.getName())
                            //使用量used
                            .append(" used:")
                            .append(x.getUsage().getUsed() / 1024 / 1024)  //byte转M
                            .append("M")
                            //申请量total
                            .append(" committed:")
                            .append(x.getUsage().getCommitted() / 1024 / 1024)
                            .append("M")
                            //最大值max
                            .append(" max:")
                            .append(x.getUsage().getMax() / 1024 / 1024)
                            .append("M");
                    System.out.println(sb);
                });

    }
}

改下agentmain方法的逻辑,调用上面打印内存信息的方法:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        MemoryCommand.printMemory();
    }

}

连接一个PID试试:
在这里插入图片描述
在这里插入图片描述

3、实现:查看直接内存

继续用JMX技术来实现:

//加载这个类(它里面包含直接内存的使用情况),获取class对象
Class bufferPoolMXBeanClass = Class.forName("java.lang.management.BufferPoolMXBean");

//getPlatformMXBeans允许传入一个MXbean的Class对象,并获取到这个MXbean,因为可能有多个,所以返回一个List
List<BufferPoolMXBean>bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(bufferPoolMXBeanclass)

如此,通过BufferPoolMXBean的这个MXbean,可获取JVM中分配的直接内存和内存映射缓冲区(这个区用于提升大文件读写性能)等的大小。具体实现:

/**
 * 打印nio相关的内存
 */
public static void printDirectMemory() {
    try {
        Class clazz = Class.forName("java.lang.management.BufferPoolMXBean");
        List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(clazz);
        //打印内容
        for (BufferPoolMXBean mxBean : bufferPoolMXBeans) {
            StringBuilder sb = new StringBuilder();
            sb.append("name:")
                    .append(mxBean.getName())
                    //使用量used
                    .append(" used:")
                    .append(mxBean.getMemoryUsed() / 1024 / 1024)  //byte转M
                    .append("M")
                    //容量
                    .append(" capacity:")
                    .append(mxBean.getTotalCapacity() / 1024 / 1024)
                    .append("M");
            System.out.println(sb);
        }

    } catch (Exception e) {
        e.printStackTrace();
    }

}

将这个方法直接在上面打印堆和非堆的方法里调一下。给用户的程序分配100M的直接内存,

在这里插入图片描述

动态代理一下,

在这里插入图片描述

看看效果:

在这里插入图片描述

4、实现:生成堆内存快照

关于生成内存快照:依旧调用getPlatformMXBean

//获取HotSpot虚拟机诊断用的一个MXbean对象,用这个Bean可以生成内存快照
//这里已知这个MXbean只有一个,所以掉没有s的方法,不再像上面直接内存一样返回一个List
HotspotDiagnosticMXBean hotspotDiagnosticMXBean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMxBean.class);

具体实现:

/**
 * 生成内存快照
 */
public static void heapDump(){
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
    HotSpotDiagnosticMXBean mXBean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
    //参数二,选true即只需要dump存活的对象,
    try {
        mXBean.dumpHeap(dateFormat.format(new Date()) + ".hprof",true);
    } catch (IOException e) {
        System.out.println("快照导出失败");
        e.printStackTrace();
    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        MemoryCommand.heapDump();
    }

}

输入普通应用的PID,动态代理一下:

在这里插入图片描述

导出快照文件成功:

在这里插入图片描述

5、实现:打印栈信息

用JMX的方法,还是通过对应的MXBean来获取

ManagementFactory.getThreadMXBean()

实现:

public class ThreadCommand {

    /**
     * 打印栈信息
     */
    public static void printThreadInfo() {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //参数一二分别为:当前的虚拟机VM是否能支持监视器和同步器,重载时的第三个参数是栈的深度(不传,默认是Int的最大值,如此,展示的栈信息最全,但性能不好)
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported()
                , threadMXBean.isSynchronizerUsageSupported());
        //打印线程信息,ThreadInfo对象中包括了栈名称、方法的调用等,按需自取
        for (ThreadInfo threadInfo : threadInfos) {
            //线程信息
            StringBuilder builder = new StringBuilder();
            builder.append("name: ")
                    .append(threadInfo.getThreadName())
                    .append(" threadId: ")
                    .append(threadInfo.getThreadId())
                    .append(" threadState: ")
                    .append(threadInfo.getThreadState());
            System.out.println(builder);
            //方法调用栈
            StackTraceElement[] stackTrace = threadInfo.getStackTrace();
            for (StackTraceElement stackTraceElement : stackTrace) {
                System.out.println(stackTraceElement);
            }
        }


    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        //MemoryCommand.heapDump();
        //打印栈信息
        ThreadCommand.printThreadInfo();
    }

}

输入普通应用的PID,动态代理一下:

在这里插入图片描述

打印成功:
在这里插入图片描述

6、实现:打印类加载器的信息

Java Agent中可以获得通过Java虚拟机提供的Instumentation对象获取类和类加载器的信息

在这里插入图片描述
作用:

  • redefine:重新设置类的字节码信息(Arthas热部署应该就用到了它)
  • retransform:根据现有类的字节码信息进行增强
  • 获取所有已加载的类信息
//相关文档
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html

具体实现:

public class ClassCommand {

    /**
     * 打印所有的类加载器
     * 形参直接用Instrumentation对象,在premain或者agentmain方法中,JDK会自己注入
     */
    public static void printAllClassLoader(Instrumentation inst) {
        //获取所有已加载的类
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        //用于去重,因为类加载器就那几种
        Set<ClassLoader> classLoaderSet = new HashSet<>();
        for (Class loadedClass : allLoadedClasses) {
            ClassLoader classLoader = loadedClass.getClassLoader();
            classLoaderSet.add(classLoader);
        }
        //打印类加载器
        String classLoaderInfo = classLoaderSet.stream()
                .map(x -> {
            //获取启动类加载器的结果为null,这里我直接给个固定的名字
            if (x == null) {
                return "BootStrapClassLoader";
            }
            //其他的类加载器就正常输出
            return x.getName();
        })
                //类加载器名字为空的不要
                .filter(x -> x != null)
                .distinct()
                .sorted(String::compareTo)
                .collect(Collectors.joining(","));
        System.out.println(classLoaderInfo);
    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        //MemoryCommand.heapDump();
        //打印栈信息
        //ThreadCommand.printThreadInfo();
        //打印类加载器信息
        ClassCommand.printAllClassLoader(inst);
    }

}

输入普通应用的PID,动态代理一下,普通应用打印它的类加载器成功:

在这里插入图片描述

7、实现:打印类的源码

思路:内存中存的是类的字节码信息,用Instumentation对象提供的转换器获取字节码信息

在这里插入图片描述

并用反编译工具jd-core得到源码:

//参考
https://github.com/java-decompiler/jd-core

使用jd-core,copy官方示例,Loader注意改字节码的来源,Printer重写end方法,打印反编译后的源码即可
在这里插入图片描述
jd-core的依赖:

<dependency>
     <groupId>org.jd</groupId>
     <artifactId>jd-core</artifactId>
     <version>1.1.3</version>
 </dependency>

具体实现:

public class ClassCommand {

    /**
     * 打印类的源代码
     */
    public static void printClassSourceCode(Instrumentation inst) {
        //输入全类名
        System.out.println("请输入全类名:");
        Scanner scanner = new Scanner(System.in);
        String className = scanner.next();
        //获取所有已加载的类,从中找到用户要的类的class对象
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class loadedClass : allLoadedClasses) {
            //找到了
            if (className.equals(loadedClass.getName())) {
                //转换器
                ClassFileTransformer transformer = new ClassFileTransformer() {
                    @Override
                    //transform方法传入一个字节码信息,返回一个增强后的字节码信息,以下代码的写法,返回null即不增强,这里只要原来的字节码
                    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                        //通过jd-code反编译,打印出源码
                        printJdCoreSourceCode(classfileBuffer, className);
                        return ClassFileTransformer.super.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); //直接调用以有的父类,返回null,即不改,因为这里只要获取最初的字节码

                    }
                };
                //添加转换器,让转换器生效,参数二为true即可手动触发
                inst.addTransformer(transformer, true);
                //触发转换器
                try {
                    inst.retransformClasses(loadedClass);
                } catch (UnmodifiableClassException e) {
                    e.printStackTrace();
                } finally {
                    //使用完之后删除转换器
                    inst.removeTransformer(transformer);
                }
            }

        }

    }

    /**
     * jd-code打印源码
     * bytes 字节码信息
     */
    private static void printJdCoreSourceCode(byte[] bytes, String className) {
        //loader对象
        Loader loader = new Loader() {
            @Override
            public byte[] load(String internalName) throws LoaderException {
                return bytes;
            }

            @Override
            public boolean canLoad(String internalName) {
                return true;    //类可加载
            }
        };
        //Printer对象,注意重写end方法,打印源代码
        Printer printer = new Printer() {
            protected static final String TAB = "  ";
            protected static final String NEWLINE = "\n";

            protected int indentationCount = 0;
            protected StringBuilder sb = new StringBuilder();

            @Override
            public String toString() {
                return sb.toString();
            }

            @Override
            public void start(int maxLineNumber, int majorVersion, int minorVersion) {
            }

            @Override
            public void end() {
                //打印源代码
                System.out.println(sb);
            }

            @Override
            public void printText(String text) {
                sb.append(text);
            }

            @Override
            public void printNumericConstant(String constant) {
                sb.append(constant);
            }

            @Override
            public void printStringConstant(String constant, String ownerInternalName) {
                sb.append(constant);
            }

            @Override
            public void printKeyword(String keyword) {
                sb.append(keyword);
            }

            @Override
            public void printDeclaration(int type, String internalTypeName, String name, String descriptor) {
                sb.append(name);
            }

            @Override
            public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) {
                sb.append(name);
            }

            @Override
            public void indent() {
                this.indentationCount++;
            }

            @Override
            public void unindent() {
                this.indentationCount--;
            }

            @Override
            public void startLine(int lineNumber) {
                for (int i = 0; i < indentationCount; i++) sb.append(TAB);
            }

            @Override
            public void endLine() {
                sb.append(NEWLINE);
            }

            @Override
            public void extraLine(int count) {
                while (count-- > 0) sb.append(NEWLINE);
            }

            @Override
            public void startMarker(int type) {
            }

            @Override
            public void endMarker(int type) {
            }
        };
        //通过jd-code打印
        ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler();

        try {
            decompiler.decompile(loader, printer, className);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        //MemoryCommand.heapDump();
        //打印栈信息
        //ThreadCommand.printThreadInfo();
        //打印类加载器信息
        //ClassCommand.printAllClassLoader(inst);
        //打印源码
        ClassCommand.printClassSourceCode(inst);
    }

}

输入普通应用的PID,动态代理一下,源码打印成功:
在这里插入图片描述

8、需求:打印方法的耗时

打印方法执行的参数和耗时,就需要对原始方法做增强。这里用字节码增强框架ASM和ByteBuddy实现。(不用Java Agent,不考虑无侵入式的话可以在自己项目中用Spring AOP,通过切面+代理对象实现)
【具体实现:字节码增强框架ASM + Byte Buddy】

9、打包成工具

改下agentmain方法,将所有功能串起来:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("菜单:\n" +
                    "1、查看内存使用情况\n"
                    + "2、生成堆内存快照\n"
                    + "3、打印栈信息\n"
                    + "4、打印类加载器\n"
                    + "5、打印类源码\n"
                    + "6、打印方法的参数和耗时\n"
                    + "7、退出\n"
            );
            String input = scanner.next();
            switch (input) {
                case "1": {
                    MemoryCommand.printMemory();
                    break;
                }
                case "2": {
                    MemoryCommand.heapDump();
                    break;
                }
                case "3": {
                    ThreadCommand.printThreadInfo();
                    break;
                }
                case "4": {
                    ClassCommand.printAllClassLoader(inst);
                    break;
                }
                case "5": {
                    ClassCommand.printClassSourceCode(inst);
                    break;
                }
                case "6": {
                    ClassCommand.enhanceClassByByteBuddy(inst);
                    break;
                }
                case "7": {
                    return;
                }
            }
        }
    }
}

使用maven-shade-plugin插件可以将所有依赖打入同一个jar包中并指定入口main方法


<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>1.4</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <!--打出来的jar包的名字-->
                <finalName>llg-agent</finalName>
                <transformers>
                    <!--java -jar 启动jar包时默认的主类-->
                    <transformer
                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.llg.AttachMain</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

在这里插入图片描述

将三个jar包放到Linux上测试下效果(普通Java应用的jar、指定了main方法入口的Java Agent Jar,Java Agent的Jar)

在这里插入图片描述

启动普通Java应用,启动指定了main方法入口的Java Agent Jar:

在这里插入图片描述
在这里插入图片描述

效果:

在这里插入图片描述

最后,和Arthas相比,自定义的这个,用户的输入和控制台输出都打印在用户应用进程里,有待改进(网络相关)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建,欢迎商务合作。wx: diudiu5555

更多推荐