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 通訊,就比如兩個進程經過事先約定好的協議,對同一個文件進行讀寫操做,以達到信息的交互和共享。簡單理解成以下圖所示的模型

相關文章
相關標籤/搜索