Java類動態加載(二)——動態加載class文件

想要在jvm啓動後,動態的加載class類文件,咱們首先須要瞭解Instrumentation、Attach、Agent、VirtualMachine、ClassFileTransformer這幾個類的用法和他們之間的關係。 

Java的com.sun.tools.attach包中的VirtualMachine類,該類容許咱們經過給attach方法傳入一個jvm的pid(進程id),遠程鏈接到jvm上。而後咱們能夠經過loadAgent方法向jvm註冊一個代理程序agent,在該agent的代理程序中會獲得一個Instrumentation實例,該實例能夠在class加載前改變class的字節碼,能夠在class加載後從新加載。在調用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer接口中提供的方法進行處理。 

下面先詳細介紹下VirtualMachine、Attach、Agent、Instrumentation、ClassFileTransformer這幾個類的用法。 
1、VirtualMachine 
VirtualMachine 詳細API能夠在這裏查看: 
http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html 

VirtualMachine中的attach(String id)方法容許咱們經過jvm的pid,遠程鏈接到jvm。當經過Attach API鏈接到JVM的進程上後,系統會加載management-agent.jar,而後在JVM中啓動一個Jmx代理,最後經過Jmx鏈接到虛擬機。 

下面展現經過attach到目標jvm,而後經過loadAgent註冊management-agent.jar代理程序,啓動jmx代理服務。 
html

Java代碼  收藏代碼java

  1. // 被監控jvm的pid(windows上能夠經過任務管理器查看)  程序員

  2.             String targetVmPid = "5936";  windows

  3.             // Attach到被監控的JVM進程上  api

  4.             VirtualMachine virtualmachine = VirtualMachine.attach(targetVmPid);  數組

  5.   

  6.             // 讓JVM加載jmx Agent  oracle

  7.             String javaHome = virtualmachine.getSystemProperties().getProperty("java.home");  app

  8.             String jmxAgent = javaHome + File.separator + "lib" + File.separator + "management-agent.jar";  jvm

  9.             virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");  函數

  10.   

  11.             // 得到鏈接地址  

  12.             Properties properties = virtualmachine.getAgentProperties();  

  13.             String address = (String) properties.get("com.sun.management.jmxremote.localConnectorAddress");  

  14.   

  15.             // Detach  

  16.             virtualmachine.detach();  

  17.             // 經過jxm address來獲取RuntimeMXBean對象,從而獲得虛擬機運行時相關信息  

  18.             JMXServiceURL url = new JMXServiceURL(address);  

  19.             JMXConnector connector = JMXConnectorFactory.connect(url);  

  20.             RuntimeMXBean rmxb = ManagementFactory.newPlatformMXBeanProxy(connector.getMBeanServerConnection(), "java.lang:type=Runtime",  

  21.                     RuntimeMXBean.class);  

  22.             // 獲得目標虛擬機佔用cpu時間  

  23.             System.out.println(rmxb.getUptime());  



位於jre\lib目錄中的management-agent.jar是沒有任何class類文件的,整個jar包中只有MANIFEST.MF文件,文件內容以下: 

Java代碼  收藏代碼

  1. Manifest-Version: 1.0  

  2. Created-By: 1.6.0 (Sun Microsystems Inc.)  

  3. Agent-Class: sun.management.Agent  

  4. Premain-Class: sun.management.Agent  


關於更多的JVM Management API(JVM管理工具API及用法請參考下面URI) 
http://ayufox.iteye.com/blog/653214 

2、Agent類 
目前Agent類的啓動有兩種方式,一種是在JDK5版本中提供隨JVM啓動的Agent,咱們稱之爲premain方式。另外一種是在JDK6中在JDK5的基礎之上又提供了JVM啓動以後經過Attach去加載的Agent類,咱們稱之爲agentmain方式。 

Agent類的兩種實現方式: 
在這兩種啓動方式下,Agent JAR文件中的代理類中都必須實現特定的方法,以下所示: 
一、隨JVM啓動的Agent方式必須實現下面兩個方法中的其中一個: 

Java代碼  收藏代碼

  1. public static void premain(String agentArgs, Instrumentation inst);[1]  

  2. public static void premain(String agentArgs);[2]  


JVM 首先嚐試在代理類上調用如下方法: 

Java代碼  收藏代碼

  1. public static void premain(String agentArgs, Instrumentation inst);  


若是代理類沒有實現此方法,那麼 JVM 將嘗試調用: 

Java代碼  收藏代碼

  1. public static void premain(String agentArgs);  




二、經過Attach去啓動Agent類方式必須實現下面兩個方法中的其中一個: 

Java代碼  收藏代碼

  1. public static void agentmain (String agentArgs, Instrumentation inst);[1]   

  2. public static void agentmain (String agentArgs);[2]   


代理類必須實現公共靜態agentmain方法。系統類加載器(ClassLoader.getSystemClassLoader)必須支持將代理 JAR 文件添加到系統類路徑的機制。代理 JAR 將被添加到系統類路徑。系統類路徑是一般加載包含應用程序 main 方法的類的類路徑。代理類將被加載,JVM 嘗試調用agentmain 方法。JVM 首先嚐試對代理類調用如下方法: 

Java代碼  收藏代碼

  1. public static void agentmain(String agentArgs, Instrumentation inst);  


若是代理類沒有實現此方法,那麼 JVM 將嘗試調用: 

Java代碼  收藏代碼

  1. public static void agentmain(String agentArgs);  


若是是使用命令行選項啓動代理,那麼agentmain方法將不會被調用。 

代理類agent的加載: 
代理類將被系統類加載器加載(參見 ClassLoader.getSystemClassLoader),系統類加載器是一般加載包含應用程序main方法的類的類加載器。 

MANIFEST.MF文件配置: 
Agent類(又稱爲代理類)必須被部署爲JAR 文件。Agent代理類jar包中的MANIFEST.MF文件中,必須指定Premain-Class或者Agent-Class參數。MANIFEST.MF文件內容以下: 

Java代碼  收藏代碼

  1. Manifest-Version: 1.0  

  2. Created-By: 1.6.0 (Sun Microsystems Inc.)  

  3. Agent-Class: sun.management.Agent  

  4. Premain-Class: sun.management.Agent  



Premain-Class 
若是 JVM 啓動時指定了代理,那麼此屬性指定代理類,即包含 premain 方法的類。若是 JVM 啓動時指定了代理,那麼此屬性是必需的。若是該屬性不存在,那麼 JVM 將停止。注:此屬性是類名,不是文件名或路徑。 

Agent-Class 
若是實現支持 VM 啓動以後某一時刻啓動代理的機制,那麼此屬性指定代理類。 即包含 agentmain 方法的類。 此屬性是必需的,若是不存在,代理將沒法啓動。 注:這是類名,而不是文件名或路徑。 

兩種代理模式的啓動方式: 
一、premain啓動代理的方式: 
在jvm的啓動參數中加入 

Java代碼  收藏代碼

  1. -javaagent:jarpath[=options]  


jarpath 是代理 JAR 文件的路徑,options 是代理選項。此開關能夠在同一代碼行使用屢次,從而建立多個代理。多個代理可使用相同的 jarpath。代理 JAR 文件必須遵照 JAR 文件規範。代理類必須實現公共靜態premain 方法,該方法的原理與main應用程序入口點相似。在 Java 虛擬機 (JVM) 初始化後,每一個 premain 方法將按照指定代理的順序調用,而後將調用實際的應用程序 main 方法。每一個 premain 方法必須按照依次進行的啓動順序返回。

-javaagent使用方法 
一個java程序中-javaagent這個參數的個數是沒有限制的,因此能夠添加任意多個java agent。 
全部的java agent會按照你定義的順序執行。 
例如: 

Java代碼  收藏代碼

  1. java -javaagent:MyAgent1.jar -javaagent:MyAgent2.jar -jar MyProgram.jar  


假設MyProgram.jar裏面的main函數在MyProgram中。 
MyAgent1.jar, MyAgent2.jar,  這2個jar包中實現了premain的類分別是MyAgent1, MyAgent2 
程序執行的順序將會是 
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main 
另外,放在main函數以後的premain是不會被執行的, 
例如 

Java代碼  收藏代碼

  1. java -javaagent:MyAgent1.jar  -jar MyProgram.jar -javaagent:MyAgent2.jar  


MyAgent2 和MyAgent3 都放在了MyProgram.jar後面,因此MyAgent2的premain都不會被執行, 
因此執行的結果將是 
MyAgent1.premain -> MyProgram.main 
每個java agent 均可以接收一個字符串類型的參數,也就是premain中的agentArgs,這個agentArgs是經過java option中定義的。 
如: 

Java代碼  收藏代碼

  1. java -javaagent:MyAgent2.jar=thisIsAgentArgs -jar MyProgram.jar  


MyAgent2中premain接收到的agentArgs的值將是」thisIsAgentArgs」 (不包括雙引號) 

二、agentmain啓動代理的方式: 
先經過VirtualMachine.attach(targetVmPid)鏈接到虛擬機,而後經過virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");註冊agent代理類。 

Java代碼  收藏代碼

  1. // 被監控jvm的pid(windows上能夠經過任務管理器查看)  

  2.         String targetVmPid = "5936";  

  3.         // Attach到被監控的JVM進程上  

  4.         VirtualMachine virtualmachine = VirtualMachine.attach(targetVmPid);  

  5.   

  6.         // 讓JVM加載jmx Agent  

  7.         String javaHome = virtualmachine.getSystemProperties().getProperty("java.home");  

  8.         String jmxAgent = javaHome + File.separator + "lib" + File.separator + "management-agent.jar";  

  9.         virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");  



代理類的方法中的參數中的Instrumentation: 
經過參數中的Instrumentation inst,添加本身定義的ClassFileTransformer,來改變class文件。這裏自定義的Transformer實現了transform方法,在該方法中提供了對實際要執行的類的字節碼的修改,甚至能夠達到執行另外的類方法的地步 

關於更多的Agent代理類的使用方法請參考下面的URI: 
http://blog.sina.com.cn/s/blog_605f5b4f0100qfvc.html 
http://mgoann.iteye.com/blog/1422680 

3、Instrumentation 
java.lang.Instrument包是在JDK5引入的,程序員經過修改方法的字節碼實現動態修改類代碼。在代理類的方法中的參數中,就有Instrumentation inst實例。經過該實例,咱們能夠調用Instrumentation提供的各類接口。好比調用inst.getAllLoadedClasses()獲得全部已經加載過的類。調用inst.addTransformer(new SdlTransformer(), true)增長一個可重轉換轉換器。調用inst.retransformClasses(Class cls),向jvm發起重轉換請求。 

Java Instrutment只提供了JVM TI中很是小的一個功能子集,一個是容許在類加載以前,修改類字節(ClassFileTransformer)(JDK5中開始提供,即便隨JVM啓動的Agent),另一個是在類加載以後,觸發JVM從新進行類加載(JDK6中開始提供,用於JVM啓動以後經過Attach去加載Agent)。這兩個功能表面看起來微不足道,但實際很是強大,AspectJ AOP的動態Weaving、Visual VM的性能剖析、JConsole支持Attach到進程上進行監控,都是經過這種方式來作的。除了這兩個功能外,JDK 6中還提供了動態增長BootstrapClassLoader/SystemClassLoader的搜索路徑、對Native方法進行instrutment(還記得JVM TI的Native Method Bind嗎?)。 
      1.主要API(java.lang.instrutment) 
      1)ClassFileTransformer:定義了類加載前的預處理類,能夠在這個類中對要加載的類的字節碼作一些處理,譬如進行字節碼加強 
      2)Instrutmentation:加強器,由JVM在入口參數中傳遞給咱們,提供了以下的功能 

  • addTransformer/ removeTransformer:註冊/刪除ClassFileTransformer

  • retransformClasses:對於已經加載的類從新進行轉換處理,即會觸發從新加載類定義,須要注意的是,新加載的類不能修改舊有的類聲明,譬如不能增長屬性、不能修改方法聲明

  • redefineClasses:與如上相似,但不是從新進行轉換處理,而是直接把處理結果(bytecode)直接給JVM

  • getAllLoadedClasses:得到當前已經加載的Class,可配合retransformClasses使用

  • getInitiatedClasses:得到由某個特定的ClassLoader加載的類定義

  • getObjectSize:得到一個對象佔用的空間,包括其引用的對象

  • appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch:增長BootstrapClassLoader/SystemClassLoader的搜索路徑

  • isNativeMethodPrefixSupported/setNativeMethodPrefix:支持攔截Native Method



關於更多的Agent代理類的使用方法請參考下面的URI: 
http://ayufox.iteye.com/blog/655619 
http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html 

4、ClassFileTransformer 

Java代碼  收藏代碼

  1. byte[] transform(ClassLoader loader,String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer)throws IllegalClassFormatException  


該接口只定義個一個方法transform,該方法會在加載新class類或者從新加載class類時,調用。例如,inst.addTransformer(new SdlTransformer(), true)當代碼中增長了一個可重轉換轉換器後,每次類加載以前,就會調用transform方法。若該方法返回null,則不改變加載的class字節碼,若返回一個byte[]數組,則jvm將會用返回的byte[]數組替換掉原先應該加載的字節碼。 

下面將transform的官方說明貼出來: 
byte[] transform(ClassLoader loader, 
                 String className, 
                 Class<?> classBeingRedefined, 
                 ProtectionDomain protectionDomain, 
                 byte[] classfileBuffer) 
                 throws IllegalClassFormatException此方法的實現能夠轉換提供的類文件,並返回一個新的替換類文件。 
有兩種裝換器,由 Instrumentation.addTransformer(ClassFileTransformer,boolean) 的 canRetransform 參數肯定: 

可重轉換 轉換器,將 canRetransform 設爲 true 可添加這種轉換器 
不可重轉換 轉換器,將 canRetransform 設爲 false 或者使用 Instrumentation.addTransformer(ClassFileTransformer) 可添加這種轉換器 
在轉換器使用 addTransformer 註冊以後,每次定義新類和重定義類時都將調用該轉換器。每次重轉換類時還將調用可重轉換轉換器。對新類定義的請求經過 ClassLoader.defineClass 或其本機等價方法進行。對類重定義的請求經過 Instrumentation.redefineClasses 或其本機等價方法進行。對類重轉換的請求將經過 Instrumentation.retransformClasses 或其本機等價方法進行。轉換器是在驗證或應用類文件字節以前的請求處理過程當中調用的。 當存在多個轉換器時,轉換將由 transform 調用鏈組成。也就是說,一個 transform 調用返回的 byte 數組將成爲下一個調用的輸入(經過 classfileBuffer 參數)。 

轉換將按如下順序應用: 

不可重轉換轉換器 
不可重轉換本機轉換器 
可重轉換轉換器 
可重轉換本機轉換器 
對於重轉換,不會調用不可重轉換轉換器,而是重用前一個轉換的結果。對於全部其餘狀況,調用此方法。在每一個這種調用組中,轉換器將按照註冊的順序調用。本機轉換器由 Java 虛擬機 Tool 接口中的 ClassFileLoadHook 事件提供。 

第一個轉換器的輸入(經過 classfileBuffer 參數)以下: 

對於新的類定義,是傳遞給 ClassLoader.defineClass 的 byte 
對於類重定義,是 definitions.getDefinitionClassFile(),其中 definitions 是 Instrumentation.redefineClasses 的參數 
對於類重轉換,是傳遞給新類定義的 byte,或者是最後一個重定義(若是有重定義),全部不可轉換轉換器進行的轉換都將自動從新應用並保持不變;有關細節,請參閱 Instrumentation.retransformClasses 
若是實現方法肯定不須要進行轉換,則應返回 null。不然,它將建立一個新的 byte[] 數組,將輸入 classfileBuffer 連同全部須要的轉換複製到其中,並返回這個新數組。不得修改輸入 classfileBuffer。 

在重轉換和重定義中,轉換器必須支持重定義語義:若是轉換器在初始定義期間更改的類在之後要重轉換或重定義,那麼轉換器必須確保第二個輸出類文件是第一個輸出類文件的合法重定義文件。 

若是轉換器拋出異常(未捕獲的異常),後續轉換器仍然將被調用並加載,仍然將嘗試重定義或重轉換。所以,拋出異常與返回 null 的效果相同。若要使用轉換器代碼在生成未檢驗異常時防止不但願發生的行爲,可讓轉換器捕獲 Throwable。若是轉換器認爲 classFileBuffer 不表示一個有效格式的類文件,則將拋出 IllegalClassFormatException;儘管這與返回 null 的效果相同,但它便於對格式毀壞進行記錄或調試。 


參數: 
loader - 定義要轉換的類加載器;若是是引導加載器,則爲 null 
className - 徹底限定類內部形式的類名稱和 The Java Virtual Machine Specification 中定義的接口名稱。例如,"java/util/List"。 
classBeingRedefined - 若是是被重定義或重轉換觸發,則爲重定義或重轉換的類;若是是類加載,則爲 null 
protectionDomain - 要定義或重定義的類的保護域 
classfileBuffer - 類文件格式的輸入字節緩衝區(不得修改) 
返回: 
一個格式良好的類文件緩衝區(轉換的結果),若是未執行轉換,則返回 null。 
拋出: 
IllegalClassFormatException - 若是輸入不表示一個格式良好的類文件 
另請參見: 
Instrumentation.redefineClasses(java.lang.instrument.ClassDefinition...) 


參考文檔: 
http://ayufox.iteye.com/blog/653214 
http://ayufox.iteye.com/blog/655619 
http://blog.sina.com.cn/s/blog_605f5b4f0100qfvc.html 
http://mgoann.iteye.com/blog/1422680 
http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

相關文章
相關標籤/搜索