Instrumentation接口详解
Posted jackion5
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Instrumentation接口详解相关的知识,希望对你有一定的参考价值。
Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以独立于应用程序之外的代理程序,可以用来监控和扩展JVM上运行的应用程序,相当于是JVM层面的AOP
功能:
监控和扩展JVM上的运行程序,替换和修改java类定义,提供一套代理机制,支持独立于JVM应用程序之外的程序以代理的方式连接和访问JVM。
比如说一个Java程序在JVM上运行,这时如果需要监控JVM的状态,除了使用JDK自带的jps等命令之外,就可以通过instrument来更直观的获取JVM的运行情况;
或者一个Java方法在JVM中执行,如果我想获取这个方法的执行时间又不想改代码,常用的做法是通过Spring的AOP来实现,而AOP通过面向切面编程,实际上编译出来的类中代码也是被改动的,而instrument是在JVM层面上直接改动java方法来实现
一、Instrumentation接口源码
源码如下:
1 public interface Instrumentation 2 { 3 //添加ClassFileTransformer 4 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); 5 6 //添加ClassFileTransformer 7 void addTransformer(ClassFileTransformer transformer); 8 9 //移除ClassFileTransformer 10 boolean removeTransformer(ClassFileTransformer transformer); 11 12 //是否可以被重新定义 13 boolean isRetransformClassesSupported(); 14 15 //重新定义Class文件 16 void redefineClasses(ClassDefinition... definitions) 17 throws ClassNotFoundException, UnmodifiableClassException; 18 19 //是否可以修改Class文件 20 boolean isModifiableClass(Class<?> theClass); 21 22 //获取所有加载的Class 23 @SuppressWarnings("rawtypes") 24 Class[] getAllLoadedClasses(); 25 26 //获取指定类加载器已经初始化的类 27 @SuppressWarnings("rawtypes") 28 Class[] getInitiatedClasses(ClassLoader loader); 29 30 //获取某个对象的大小 31 long getObjectSize(Object objectToSize); 32 33 //添加指定jar包到启动类加载器检索路径 34 void appendToBootstrapClassLoaderSearch(JarFile jarfile); 35 36 //添加指定jar包到系统类加载检索路径 37 void appendToSystemClassLoaderSearch(JarFile jarfile); 38 39 //本地方法是否支持前缀 40 boolean isNativeMethodPrefixSupported(); 41 42 //设置本地方法前缀,一般用于按前缀做匹配操作 43 void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); 44 }
主要是定义了操作java类的class文件方法,这里又涉及到了ClassFileTransformer接口,这个接口的作用是改变Class文件的字节码,返回新的字节码数组,源码如下:
1 public interface ClassFileTransformer 2 { 3 4 byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 5 ProtectionDomain protectionDomain, byte[] classfileBuffer) 6 throws IllegalClassFormatException; 7 }
ClassFileTransformer接口只有一个方法,就是改变指定类的Class文件,该接口没有默认实现,很显然如果需要改变Class文件的内容,需要改成什么样需要使用者自己来实现。
二、Instrumentation接口的使用案例
Instrumentation可以在带有main方法的应用程序之前运行,通过-javaagent参数来指定一个特点的jar文件(包含Instrumentation代理)来启动Instrumentation的代理程序,所以首先需要编写一个Instrumentation的代理程序,案例如下:
新建代理项目
1 public class MyAgent 2 { 3 /** 4 * 参数args是启动参数 5 * 参数inst是JVM启动时传入的Instrumentation实现 6 * */ 7 public static void premain(String args,Instrumentation inst) 8 { 9 System.out.println("premain方法会在main方法之前执行......"); 10 inst.addTransformer(new MyTransformClass()); 11 } 12 } 13 14 ------------------------------------------------------------------------ 15 public class MyTransformClass implements ClassFileTransformer 16 { 17 18 @Override 19 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 20 ProtectionDomain protectionDomain, byte[] classfileBuffer) 21 throws IllegalClassFormatException 22 { 23 // 定义重新编译之后的字符流数组 24 byte[] newClassFileBuffer = new byte[classfileBuffer.length]; 25 String transClassName = "com.mrhu.opin.controller.TestController";//重定义指定类,也可以重定义指定package下的类,使用者自由发挥 26 if (className.equals(transClassName)) 27 { 28 System.out.println("监控到目标类,重新编辑Class文件字符流..."); 29 // TODO 对目标类的Class文件字节流进行重新编辑 30 // 对byte[]重新编译可以使用第三方工具如javassist,感兴趣的可自行研究 31 // 本文图方便,直接返回旧的字节数组 32 newClassFileBuffer = classfileBuffer; 33 } 34 return newClassFileBuffer; 35 } 36 37 }
编译打包项目为 instrumentdemo.jar,然后其他在需要被监控的项目启动参数中添加如下参数:
-javaagent:instrumentdemo.jar
然后在被监控应用程序执行main方法之前就会先执行premain方法,走instrumentation代理程序,那么在应用程序加载类的时候就会进入到自定义的ClassFileTransformer中
Instrumentation还可以添加多个代理,按照代理指定的顺序依次调用
(详细案例可以自行百度了解,本文只做理论描述)
三、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来进行回滚操作,可以回滚到字节码之前的版本。
以上是关于Instrumentation接口详解的主要内容,如果未能解决你的问题,请参考以下文章