Java Agent的运行方式
- JVM并不会限制Java Agent的数量
- 可以在JVM参数中包含多个-javaagent参数
- 也可以远程attach多个Java Agent
- JVM会按照参数的顺序或者attach的顺序,逐个执行Java Agent
- JRebal/Btrace/arthas等工具都是基于Java Agent实现的
premain
以JVM参数(-javaagent)的方式启动,在Java程序的main方法执行之前执行
MyAgent
1 | package me.zhongmingmao; |
manifest.txt
1 | # 写入两行数据,最后一行为空行 |
编译打包
1 | $ javac me/zhongmingmao/MyAgent.java |
HelloWorld
1 | package helloworld; |
编译运行
1 | $ javac helloworld/HelloWorld.java |
agentmain
- 以Attach的方式启动,在Java程序启动后运行,利用VirtualMachine的Attach API
- Attach API其实是Java进程之间的沟通桥梁,底层通过Socket进行通信
- jps/jmap/jinfo/jstack/jcmd均依赖于Attach API
MyAgent
1 | package me.zhongmingmao; |
manifest.txt
1 | # 改为Agent-Class |
编译打包
1 | $ javac me/zhongmingmao/MyAgent.java |
AttachTest
1 | import com.sun.tools.attach.VirtualMachine; |
编译AttachTest
1 | # 指定classpath |
运行HelloWorld
1 | $ java helloworld.HelloWorld |
运行AttachTest
1 | $ java -cp ~/.sdkman/candidates/java/current/lib/tools.jar:. AttachTest 23386 PATH_TO_AGENT/myagent.jar |
1 | # HelloWorld进程继续输出agentmain |
Java Agent的功能
- ClassFileTransformer用于拦截类加载事件,需要注册到Instrumentation
- Instrumentation.redefineClasses
- 针对已加载的类,舍弃原本的字节码,替换为由用户提供的byte数组
- 功能比较危险,一般用于修复出错的字节码
- Instrumentation.retransformClasses
- 针对已加载的类,重新调用所有已注册的ClassFileTransformer的transform方法,两个场景
- 在执行premain和agentmain方法前,JVM已经加载了不少类
- 而这些类的加载事件并没有被拦截并执行相关的注入逻辑
- 定义了多个Java Agent,多个注入的情况,可能需要移除其中的部分注入
- 调用Instrumentation.removeTransformer去除某个注入类后,可以调用retransformClasses
- 重新从原始byte数组开始进行注入
- Java Agent的功能是通过JVMTI Agent(C Agent),JVMTI是一个事件驱动的工具实现接口
- 通常会在C Agent加载后的方法入口Agent_OnLoad处注册各种事件的钩子方法
- 当JVM触发这些事件时,便会调用对应的钩子方法
- 例如可以为JVMTI中的ClassFileLoadHook事件设置钩子,从而在C层面拦截所有的类加载事件
获取魔数
MyAgent
1 | package me.zhongmingmao; |
编译运行
1 | $ java -javaagent:myagent.jar helloworld.HelloWorld |
ASM注入字节码
通过ASM注入字节码可参考Instrumenting Java Bytecode with ASM
MyAgent
1 | package me.zhongmingmao; |
编译MyAgent
1 | $ javac -cp PATH_TO_ASM/asm-7.0.jar:PATH_TO_ASM_TREE/asm-tree-7.0.jar me/zhongmingmao/MyAgent.java |
运行
1 | $ java -javaagent:myagent.jar -cp PATH_TO_ASM/asm-7.0.jar:PATH_TO_ASM_TREE/asm-tree-7.0.jar:. helloworld.HelloWorld |
基于字节码注入的profiler
统计新建实例数量
MyProfiler
1 | package me.zhongmingmao; |
MyAgent
1 | package me.zhongmingmao; |
ProfilerMain
1 | public class ProfilerMain { |
运行
1 | $ java -javaagent:myagent.jar -cp PATH_TO_ASM/asm-7.0.jar:PATH_TO_ASM_TREE/asm-tree-7.0.jar:. ProfilerMain |
命名空间
- 不少应用程序都依赖于ASM工程,当注入逻辑依赖于ASM时
- 可能会出现注入使用最新版的ASM,而应用程序本身使用的是较低版本的ASM
- JDK本身也使用了ASM库,例如用来生成Lambda表达式的适配器,JDK的做法是重命名整个ASM库
- 为所有类的包名添加jdk.internal前缀
- 还有另外一个方法是借助自定义类加载器来隔离命名空间
观察者效应
- 例如字节码注入收集每个方法的运行时间
- 假设某个方法调用了另一个方法,而这个两个方法都被注入了
- 那么统计被调用者运行时间点注入代码所耗费的时间,将不可避免地被计入至调用者方法的运行时间之中
- 统计新建对象数量
- 即时编译器的逃逸分析可能会优化掉新建对象操作,但它并不会消除相关的统计操作
- 因此会统计到实际没有发生的新建对象操作
- 因此当使用字节码注入开发profiler,仅能表示在被注入的情况下程序的执行状态