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 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能夠在帶有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的原理,就不得不先提起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命令來加載
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裏的內容
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方法
啓動時加載和運行時加載都是監聽同一個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 通訊,就比如兩個進程經過事先約定好的協議,對同一個文件進行讀寫操做,以達到信息的交互和共享。簡單理解成以下圖所示的模型