我们在项目调试、排查问题、性能监控的时候经常会用到一些开源工具,比如btrace、jprofile、arthas。今天这篇文章就带你了解下这些工具背后的实现原理:Java Instrumentation
1、Java Instrumentation
Instrument是jvm 提供的一个可以修改已加载类的类库,专门为java语言提供的插桩服务提供支持。
使用 Instrumentation,可以替换和修改某些类的定义、向classLoader的classpath下加入jar文件等。我们构建一个独立于应用程序的代理程序(Agent),进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。
Instrumentation 类有常用的方法:
方法 | 说明 |
---|---|
void addTransformer(ClassFileTransformer transformer) | 注册提供的转换器 |
long getObjectSize(Object objectToSize) | 返回指定类的大小 |
void retransformClasses(Class<?>… classes) | 重转换提供的类集 |
Class[] getAllLoadedClasses() | 获取当前加载的所有类 |
void appendToBootstrapClassLoaderSearch(JarFile jarfile) | 将一个jar加入到bootstrap classloader的 classpath里 |
removeTransformer | 移除转换器 |
redefineClasses | 重定义class,会stw |
我们常用的是 addTransformer 方法,它会注册一个ClassFileTransformer,在ClassFileTransformer 里可以直接对指定类的类字节码进行修改。通常来说,一般使用ASM,CGLIB,Byte Buddy,Javassist等框架进行字节码增强。
1 |
|
2、Java Agent
上面的例子在ClassFileTransformer 里实现对字节码的增强,此时我们需要通过Java Agent 拿到一个Instrumentation实例,来实现增强的功能。
Java Agent是一种特殊的jar文件,它是Instrumentation的客户端。根据不同的启动时机,Agent类需要实现不同的方法(二选一)。1
2
3
4//静态加载
public static void premain(String agentArgs, Instrumentation inst);
//动态加载
public static void agentmain(String agentArgs, Instrumentation inst);
方法 | 说明 |
---|---|
pemain 静态加载 | 以vm参数的形式载入。在程序main方法执行之前执行。其jar包的manifest需要配置属性Premain-Class。如果premain方法执行失败或者抛出异常,那么启动就会失败。 |
agentmain 动态加载 | 以Attach的方式载入,在Java程序启动后执行。其jar包的manifest需要配置属性Agent-Class。如果执行失败或者抛出异常,那么jvm会忽略错误,不会影响程序的正常运行。 |
1 | /** |
manifest配置则可以参考1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.example.test.AgentTest</Premain-Class>
<Agent-Class>com.example.test.AgentTest</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
按照上述步骤实现了Agent后,需要打出Agent jar包。然后看你是需要静态加载还是动态加载。
- 静态加载:使用vm参数(如
java -javaagent:agent.jar -jar application.jar
)跟随宿主程序一起启动 - 动态加载:使用 jvm attach机制启动
3、Attach机制
1 | //使用例子 |
VirtualMachine.attach动作类似TCP创建连接的三次握手,目的就是与target VM搭建attach通信的连接。而后面执行的操作,例如vm.loadAgent,其实就是向这个socket写入数据流,接收方target VM会针对不同的传入数据来做不同的处理。
上述代码里在vm.loadAgent之后,相应的agent就会被目标JVM进程加载,并执行agentmain方法。
Attach API不仅仅可以实现动态加载agent,它其实是跨JVM进程通讯的工具,能够将某种指令从一个JVM进程发送给另一个JVM进程。加载agent只是Attach API发送的各种指令中的一种, 诸如jstack打印线程栈、jps列出Java进程、jmap做内存dump等功能,都属于Attach API可以发送的指令。
这里有一篇详细讲述 java attach 的文章,值得阅读下。
4、JVMTI
JVMTI =JVM Tool Interface是JVM提供的一套对JVM进行操作的native编程接口。JVMTI是实现Java调试器,以及其它Java运行态测试与分析工具的基础。
通过JVMTI可以实现对JVM的多种操作,然后通过接口注册各种事件勾子。在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等
javaagent 也是基于JVMTI实现,借助JVMTI的一部分能力,帮助动态重载类信息
5、相关技术的实际应用
Arthas 是Alibaba开源的Java诊断工具。提供了非常强大功能。Arthas能做什么?Arthas官方文档里面是这样说的:1
2
3
4
5
6这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到JVM的实时运行状态?
除了官方文档里面提到的这几点,它还能对方法的入参、出参进行监控,记录方法内部调用路径,执行耗时,还有死锁、jar包冲突、占用CUP过大的线程分析等问题。
这是Arthas的一张说明图。
可以看出就是对Instrumentation的实际应用。一些技术细节 例如类加载的隔离值得研究学习。
这个项目已经在github开源,相关文档和教程也很丰富(Arthas。