}
编译打包项目为 instrumentdemo.jar,然后其他在需要被监控的项目启动参数中添加如下参数:
-javaagent:instrumentdemo.jar
然后在被监控应用程序执行main方法之前就会先执行premain方法,走instrumentation代理程序,那么在应用程序加载类的时候就会进入到自定义的ClassFileTransformer中
Instrumentation还可以添加多个代理,按照代理指定的顺序依次调用
(详细案例可以自行百度了解,本文只做理论描述)
所以Instrumentation接口相当于一个代理,当执行premain方法时,通过Instrumentation提供的API可以动态的添加管理JVM加载的Class文件,Instrumentation管理着ClassFileTransformer。
ClassFileTransformer接口可以动态的改变Class文件的字节码,在加载字节码的时候可以将字节码进行动态修改,具体实现需要自定义实现类来实现ClassFileTransformer接口
那么premain方法中的Instrumentation对象是如何传入的呢?答案是JVM传入的。
四、Instrumentation的实现原理
说起Instrumentation的原理,就不得不先提起JVMTI,全称是JVM Tool Interface顾名思义是JVM提供的工具接口,也就是JVM提供给用户的扩展接口集合。
JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口,这些接口可以供开发者扩展自行的逻辑。
比如我想监听JVM加载某个类的事件,那么我们就可以实现一个回调函数赋给jvmtiEnv的回调方法集合里的ClassFileLoadHook(Class类加载事件),那么当JVM进行类加载时就会触发回调函数,我们就可以在JVM加载类的时候做一些扩展操作,
比如上面提到的更改这个类的Class文件信息来增强这个类的方法。
JVMTI运行时,一个JVMTIAgent对应一个jvmtiEnv或者是多个,JVMTIAgent是一个动态库,利用JVMTI暴露出来的接口来进行扩展。
主要有三个函数:
Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的Agent_OnLoad函数(通过-agentlib加载vm参数中)
Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后给对应的目标程序发送load命令来加载,则在加载过程中会调用Agent_OnAttach方法
Agent_OnUnload方法:在agent卸载时调用
我们常用的Eclipse等调试代码实际就是使用到了这个JVMTIAgent
回到主题,Instrument 就是一种 JVMTIAgent,它实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是在使用时,Instrument既可以在启动时加载,也可以再运行时加动态加载
启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式
运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载
3.1、启动时加载
Instrument agent启动时加载会实现Agent_OnLoad方法,具体实现逻辑如下:
1.创建并初始化JPLISAgent
2.监听VMInit事件,在vm初始化完成之后执行下面逻辑
a.创建Instrumentation接口的实例,也就是InstrumentationImpl对象
b.监听ClassFileLoadHook事件(类加载事件)
c.调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
3.解析MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的内容
3.2、运行时加载
Instrument agent运行时加载会使用Agent_OnAttach方法,会通过JVM的attach机制来请求目标JVM加载对应的agent,过程如下
1.创建并初始化JPLISAgent
2.解析javaagent里的MANIFEST.MF里的参数
3.创建InstrumentationImpl对象
4.监听ClassFileLoadHook事件
5.调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
3.3、ClassFileLoadHook回调实现
启动时加载和运行时加载都是监听同一个jvmti事件那就是ClassFileLoadHook,这个是类加载的事件,在读取类文件字节码之后回调用的,这样就可以对字节码进行修改操作。
在JVM加载类文件时,执行回调,加载Instrument agent,创建Instrumentation接口的实例并且执行premain方法,premain方法中注册自定义的ClassFileTransformer来对字节码文件进行操作,这个就是在加载时进行字节码增强的过程。
那么如果java类已经加载完成了,在运行的过程中需要进行字节码增强的时候还可以使用Instrumentation接口的redifineClasses方法,有兴趣的可以自行研究源码,这里只描述大致过程。
通过执行该方法,在JVM中相当于是创建了一个VM_RedifineClasses的VM_Operation,此时会stop_the_world,具体的执行过程如下:
挨个遍历要批量重定义的 jvmtiClassDefinition
然后读取新的字节码,如果有关注 ClassFileLoadHook 事件的,还会走对应的 transform 来对新的字节码再做修改
字节码解析好,创建一个 klassOop 对象
对比新老类,并要求如下:
父类是同一个
实现的接口数也要相同,并且是相同的接口
类访问符必须一致
字段数和字段名要一致
新增的方法必须是 private static/final 的
可以删除修改方法
对新类做字节码校验
合并新老类的常量池
如果老类上有断点,那都清除掉
对老类做 JIT 去优化
对新老方法匹配的方法的 jmethodId 做更新,将老的 jmethodId 更新到新的 method 上
新类的常量池的 holer 指向老的类
将新类和老类的一些属性做交换,比如常量池,methods,内部类
初始化新的 vtable 和 itable
交换 annotation 的 method、field、paramenter
遍历所有当前类的子类,修改他们的 vtable 及 itable
上面是基本的过程,总的来说就是只更新了类里的内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新所带来的开销。
另外还可以通过retransform来进行回滚操作,可以回滚到字节码之前的版本。
------------------------------------------------------------
1. Instrumentation相当于一个JVM级别的AOP
2.Instrumentation在JVM启动的时候监听事件,如类加载事件,JVM触发来指定的事件通过回调通知,并创建一个 Instrumentation接口的实例,然后找到MANIFEST.MF中配置的实现了premain方法的Class
然后将Instrumentation实例传入premain方法中
3.premain方法会在main方法之前执行,可以添加ClassFileTransfer来实现对Class文件字节码的动态修改(并不会修改Class文件中的字节码,而是修改已经被JVM加载的字节码)
4.修改字节码的技术可以使用开源的 ASM、javassist、byteBuddy等
执行premain方法是通过在JVM启动的时候实现的动态代理,那么如果想要在JVM的运行过程中实现这个功能该如何实现呢?这就需要使用JVM的attach机制
JVM提供了一种attach机制,简单点说就是可以通过一个JVM来操作、查询另一个JVM中的数据,比如最常用的jmap、jstack等命令就是通过attach机制实现的。
当需要dump一个JVM进程中的堆信息时,此时就可以通过开启另一个JVM进程,如何通过这个JVM进程来和目标JVM进程进行通信,执行想要执行的命令或者查询想要的数据
Attach 实现的根本原理就是使用了 Linux 下是文件 Socket 通信(详情可以自行百度或 Google)。有人也许会问,为什么要采用文件 socket 而不采用网络 socket?我个人认为也许一方面是为了效率(避免了网络协议的解析、数据包的封装和解封装等),另一方面是为了减少对系统资源的占用(如网络端口占用)。采用文件 socket 通信,就好比两个进程通过事先约定好的协议,对同一个文件进行读写操作,以达到信息的交互和共享。
五、JVM的Attach机制
JDK1.6之后提供了attach机制,工具类都处于com.sun.tools.attach包下,可以通过VirtualMachine.attach方法attach到指定JVM进程上,然后获取到指定JVM的虚拟机对象VirtualMachine。代码如下:
1 VirtualMachine virtualMachine = VirtualMachine.attach("pid");
JVM进程有两个用于attach机制的线程,一个Signal Dispatcher线程,用于处理信号;一个Attach Listener线程,用于JVM进程之间的通信,可以通过配置启动参数java -XX:+StartAttachListener mainClass开启Attach Listener线程。
另外如果一个JVM被其他进程attach,那么该JVM的Signal Dispatcher线程会处理信号并启动Attach Listener线程。
Attach Listener线程启动之后,会创建监听socket,并创建了一个文件/tmp/.java_pid,这个就是LinuxVirtualMachine构造函数中一直尝试获取的socketFile。随着这个socketFile创建,也就意味着客户端那边的attach成功了。之后客户端和目标JVM进程就通过这个socketFile进行通信。客户端可以通过这个socketFile发送相关命令。Attach Listener线程做的事情就是监听这个socketFile,发现有请求就解析,然后根据命令执行不同的方法,最后将结果返回。
通过Attach机制,就可以很好的实现JDK自带的jstack、jmap等命令