Javaagent技术及Instrumentation接口详解

1、Javaagentjava

Javaagent至关于一个插件,在JVM启动的时候能够添加 javaagent配置指定启动以前须要启动的agent jar包
这个agent包中须要有MANIFEST.MF文件必须指定Premain-Class配置,且Premain-Class配置指定的Class必须实现premain()方法编程

在JVM启动的时候,会从agent包中找到MAINIFEST.MF中配置的Class,执行其实现的premain方法,并且这一步是在main方法以前执行的。
这样就能够在JVM启动执行main方法以前作一些其余而外的操做了。数组

premain方法有两种网络

public static void premain(String agentArgs, Instrumentation inst){
      //执行main方法以前的操做
}

public static void premain(String agentArgs){
      //执行main方法以前的操做
}

 

agent会优先执行第一个方法,若是第二个方法不存在则才会执行第二个方法。app

javaagent使用的步骤主要以下:jvm

一、新建agent项目,新建自定义agent的入口类,以下socket

 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方法以前执行......");11     }
12 }

二、编辑MANIFEST.MF文件,内容以下:ide

Mainfest-version: 1.0
Premain-class: cn.lucky.test.agent.MyAgent

三、将agent项目打包成自定义的名字,如 myagent.jar函数

四、在目标项目启动的时候添加JVM参数工具

-javaagent: myagent.jar

简单的四步就实现了一个自定义的javaagent,agent的具体实现功能就看自定义的时候如何实现premain(),能够premain方法中添加任何想要在main方法执行以前的逻辑。

 premain方法中有一个参数,Instrumentation,这个是才是agent实现更强大的功能都核心所在

 

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方法来实现

1、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文件的内容,须要改为什么样须要使用者本身来实现。

2、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接口至关于一个代理,当执行premain方法时,经过Instrumentation提供的API能够动态的添加管理JVM加载的Class文件,Instrumentation管理着ClassFileTransformer。

ClassFileTransformer接口能够动态的改变Class文件的字节码,在加载字节码的时候能够将字节码进行动态修改,具体实现须要自定义实现类来实现ClassFileTransformer接口

那么premain方法中的Instrumentation对象是如何传入的呢?答案是JVM传入的。

3、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.一、启动时加载

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.二、运行时加载

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.三、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 通讯,就比如两个进程经过事先约定好的协议,对同一个文件进行读写操做,以达到信息的交互和共享。简单理解成以下图所示的模型

相关文章
相关标签/搜索