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

相关文章
相关标签/搜索