日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

Javaagent技术及Instrumentation接口详解

發布時間:2023/12/13 综合教程 35 生活家
生活随笔 收集整理的這篇文章主要介紹了 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接口详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。