【进阶篇】三、Java Agent实现自定义Arthas工具

文章目录

  • 0、客户端代码
  • 1、JMX
  • 2、实现:查看内存使用情况
  • 3、实现:查看直接内存
  • 4、实现:生成堆内存快照
  • 5、实现:打印栈信息
  • 6、实现:打印类加载器的信息
  • 7、实现:打印类的源码
  • 8、需求:打印方法的耗时

自己写一个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,通过切面+代理对象实现)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/538716.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenHarmony开发学习:【源码下载和编译】

本文介绍了如何下载鸿蒙系统源码&#xff0c;如何一次性配置可以编译三个目标平台&#xff08;Hi3516&#xff0c;Hi3518和Hi3861&#xff09;的编译环境&#xff0c;以及如何将源码编译为三个目标平台的二进制文件。 坑点总结&#xff1a; 下载源码基本上没有太多坑&#xff…

【Android surface 】二:源码分析App的surface创建过程

文章目录 画布surfaceViewRoot的创建&setView分析setViewrequestLayoutViewRoot和WMS的关系 activity的UI绘制draw surfacejni层分析Surface无参构造SurfaceSessionSurfaceSession_init surface的有参构造Surface_copyFromSurface_writeToParcelSurface_readFromParcel 总结…

无人机概述

1、中英文对照表 中文中文简称英文全称英文简称无人驾驶飞机无人机Unmanned Aerial VehicleUAV无人机自组织网络无人机网络flying Ad-Hoc networkFANET 2、相关概念 2.1鲁棒性 网络鲁棒性是指网络系统在面对随机故障、蓄意攻击或其他异常情况时&#xff0c;能够保持其基本功…

Linux的网口名字的命名规则

在工作中&#xff0c;偶尔看到有些机器的网口名字是以ethX命令&#xff0c;有些则以enpXsX这种名字命名。网上的资料说的都不太明白,资料也无据可查&#xff0c;很难让人信服。于是决定自己查了下官方的资料和源码&#xff0c;把这些搞清楚。 官方文档&#xff1a;Predictable…

MobX进阶:从基础到高级特性全面探索

MobX 提供了丰富的高级特性,包括计算属性、反应式视图、中间件、异步流程控制、依赖注入和动态 observable 、在服务端渲染和 TypeScript 支持方面提供了良好的集成。这些特性进一步增强了 MobX 在状态管理方面的灵活性和可扩展性,使其成为一个功能强大、易于使用的状态管理解决…

【NLP练习】调用Gensim库训练Word2Vec模型

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、准备工作 1.安装Gensim库 使用pip安装&#xff1a; !pip install gensim2. 对原始语料分词 选择《人民的民义》的小说原文作为语料&#xff0c;先采用…

在Windows 10中打开PowerShell的几种方法,总有一种适合你

PowerShell是一种比命令提示符更强大的命令行shell和脚本语言。自Windows10发布以来,它已成为默认选择,并且有许多方法可以打开它。 PowerShell和命令提示符之间的区别是什么 PowerShell的使用更复杂,但它比命令提示符强大得多。这就是为什么它成为超级用户和it专业人员的…

CTF之Flask_FileUpload

拿到题目就一个上传文件的网页 查看源码&#xff0c;发现注释告诉我们会运行python的文件&#xff0c;但是系统只能上传图片格式&#xff08;这个是自己尝试知道的&#xff09; 那我们就写一个python代码改成jpg或者png格式的文件 内容为 import os os.system(cat /flag) 上传…

算法 囚犯幸存者

题目 主类 public static List<Prisoner> prisoners new ArrayList<Prisoner>(); public static List<Prisoner> remainPrisoners new ArrayList<Prisoner>(); public static Prisoner lastPrisoner null;public static void main(String[] args) …

jeecg-boot安装

我看大家都挺关注&#xff0c;所以集中上传了下代码和相关工具&#xff0c;方便大家快速完成 链接&#xff1a;https://pan.baidu.com/s/1-Y9yHVZ-4DQFDjPBWUk4-A 提取码&#xff1a;op1r 1. 下载代码 下载地址 : JEECG官方网站 - 基于BPM的低代码开发平台(低代码平台_零代…

单链表的应用

上篇博客中&#xff0c;我们学习了单链表&#xff0c;为了更加熟练掌握这一知识点&#xff0c;就让我们将单链表的应用操练起来吧&#xff01; 203. 移除链表元素 - 力扣&#xff08;LeetCode&#xff09; 思路一&#xff1a;遍历原链表&#xff0c;将值为val的节点释放掉。 …

华为校园公开课走入上海交大,鸿蒙成为专业核心课程

4月12日&#xff0c;华为校园公开课在中国上海交通大学成功举办&#xff0c;吸引了来自计算机等相关专业的150余名学生参加。据了解&#xff0c;由吴帆、陈贵海、过敏意、吴晨涛、刘生钟等教授在中国上海交通大学面向计算机系本科生开设的《操作系统》课程&#xff0c;是该系学…

CS224N第二课作业--word2vec与skipgram

文章目录 CS224N: 作业2 word2vec (49 Points)1. Math: 理解 word2vec计算 J n a i v e − s o f t m a x ( v c , o , U ) J_{naive-softmax}(v_c, o, U) Jnaive−softmax​(vc​,o,U) 关于 v c v_c vc​ 的偏导数计算 J n a i v e − s o f t m a x ( v c , o , U ) J_{na…

【SpringBoot】mybatis-plus实现增删改查

mapper继承BaseMapper service 继承ServiceImpl 使用方法新增 save,updateById新增和修改方法返回boolean值,或者使用saveOrUpdate方法有id执行修改操作,没有id 执行新增操作 案例 Service public class UserService extends ServiceImpl<UserMapper,User> {// Au…

第四百五十六回

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 使用方法 3. 内容总结 我们在上一章回中介绍了"overlay_tooltip用法"相关的内容&#xff0c;本章回中将介绍onBoarding包.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中介绍的onBo…

Python 以点生成均匀二维圆点数据

已知一个数&#xff0c;但这个数不是最对的&#xff0c;最对的可能在它附近&#xff0c;想要从附近随机生成一群数&#xff0c;放入模型中暴力查找最对的那个值。 以下是代码片段 n 800 # 个数 m 2 # 角度&#xff0c;只有2才是均匀的&#xff0c;1为半圆&#xff0c;以此类…

HashMap的常见问题

Entry中的hash属性为什么不直接使用key的hashCode()返回值呢&#xff1f; 不管是JDK1.7还是JDK1.8中&#xff0c;都不是直接用key的hashCode值直接与table.length-1计算求下标的&#xff0c;而是先对key的hashCode值进行了一个运算&#xff0c;JDK1.7和JDK1.8关于hash()的实现…

1. Django建站基础

1. Django建站基础 学习开发网站必须了解网站的组成部分, 网站类型, 运行原理和开发流程. 使用Django开发网站必须掌握Django的基本操作, 比如创建项目, 使用Django的操作指令以及开发过程中的调试方法.1.1 网站的定义及组成 网站(Website)是指在因特网上根据一定的规则, 使用…

C++高级特性:柯里化过程与std::bind(六)

1、柯里化过程 1.1、operator()的引入 现在需要完成这样一个需求&#xff1a;有一个函数每次调用返回的结果不一样。例如&#xff1a;两次调用的返回值都不一样那么就可以达到这种目的 1.1.1、简单点的写法 可以给一个全局的变量&#xff08;静态变量&#xff09;&#xff…

竞赛课第六周(树状数组的应用)

实验内容: HDU 1166 敌兵布阵【线段树】 线段树的应用 敌兵布阵 C国的死对头A国这段时间正在进行军事演习&#xff0c;所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取…