Javaagent技术及Instrumentation接口详解
一、Javaagent
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會優先執行第一個方法,如果第二個方法不存在則才會執行第二個方法。
javaagent使用的步驟主要如下:
1、新建agent項目,新建自定義agent的入口類,如下
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 }
2、編輯MANIFEST.MF文件,內容如下:
Mainfest-version: 1.0 Premain-class: cn.lucky.test.agent.MyAgent
3、將agent項目打包成自定義的名字,如 myagent.jar
4、在目標項目啟動的時候添加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方法來實現
一、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接口相當于一個代理,當執行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 通信,就好比兩個進程通過事先約定好的協議,對同一個文件進行讀寫操作,以達到信息的交互和共享。簡單理解成如下圖所示的模型
總結
以上是生活随笔為你收集整理的Javaagent技术及Instrumentation接口详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (十二)洞悉linux下的Netfilt
- 下一篇: Javascript之全局变量和局部变量