想要在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这几个类的用法。
一、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代理服务。
Java代码
// 被监控jvm的pid(windows上可以通过任务管理器查看)
String targetVmPid = "5936";
// Attach到被监控的JVM进程上
VirtualMachine virtualmachine = VirtualMachine.attach(targetVmPid);
// 让JVM加载jmx Agent
String javaHome = virtualmachine.getSystemProperties().getProperty("java.home");
String jmxAgent = javaHome + File.separator + "lib" + File.separator + "management-agent.jar";
virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");
// 获得连接地址
Properties properties = virtualmachine.getAgentProperties();
String address = (String) properties.get("com.sun.management.jmxremote.localConnectorAddress");
// Detach
virtualmachine.detach();
// 通过jxm address来获取RuntimeMXBean对象,从而得到虚拟机运行时相关信息
JMXServiceURL url = new JMXServiceURL(address);
JMXConnector connector = JMXConnectorFactory.connect(url);
RuntimeMXBean rmxb = ManagementFactory.newPlatformMXBeanProxy(connector.getMBeanServerConnection(), "java.lang:type=Runtime",
RuntimeMXBean.class);
// 得到目标虚拟机占用cpu时间
System.out.println(rmxb.getUptime());
位于jre\lib目录中的management-agent.jar是没有任何class类文件的,整个jar包中只有MANIFEST.MF文件,文件内容如下:
Java代码
Manifest-Version: 1.0
Created-By: 1.6.0 (Sun Microsystems Inc.)
Agent-Class: sun.management.Agent
Premain-Class: sun.management.Agent
关于更多的JVM Management API(JVM管理工具API及用法请参考下面URI)
http://ayufox.iteye.com/blog/653214
二、Agent类
目前Agent类的启动有两种方式,一种是在JDK5版本中提供随JVM启动的Agent,我们称之为premain方式。另一种是在JDK6中在JDK5的基础之上又提供了JVM启动之后通过Attach去加载的Agent类,我们称之为agentmain方式。
Agent类的两种实现方式:
在这两种启动方式下,Agent JAR文件中的代理类中都必须实现特定的方法,如下所示:
1、随JVM启动的Agent方式必须实现下面两个方法中的其中一个:
Java代码
public static void premain(String agentArgs, Instrumentation inst);[1]
public static void premain(String agentArgs);[2]
JVM 首先尝试在代理类上调用以下方法:
Java代码
public static void premain(String agentArgs, Instrumentation inst);
如果代理类没有实现此方法,那么 JVM 将尝试调用:
Java代码
public static void premain(String agentArgs);
2、通过Attach去启动Agent类方式必须实现下面两个方法中的其中一个:
Java代码
public static void agentmain (String agentArgs, Instrumentation inst);[1]
public static void agentmain (String agentArgs);[2]
代理类必须实现公共静态agentmain方法。系统类加载器(ClassLoader.getSystemClassLoader)必须支持将代理 JAR 文件添加到系统类路径的机制。代理 JAR 将被添加到系统类路径。系统类路径是通常加载包含应用程序 main 方法的类的类路径。代理类将被加载,JVM 尝试调用agentmain 方法。JVM 首先尝试对代理类调用以下方法:
Java代码
public static void agentmain(String agentArgs, Instrumentation inst);
如果代理类没有实现此方法,那么 JVM 将尝试调用:
Java代码
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代码
Manifest-Version: 1.0
Created-By: 1.6.0 (Sun Microsystems Inc.)
Agent-Class: sun.management.Agent
Premain-Class: sun.management.Agent
Premain-Class
如果 JVM 启动时指定了代理,那么此属性指定代理类,即包含 premain 方法的类。如果 JVM 启动时指定了代理,那么此属性是必需的。如果该属性不存在,那么 JVM 将中止。注:此属性是类名,不是文件名或路径。
Agent-Class
如果实现支持 VM 启动之后某一时刻启动代理的机制,那么此属性指定代理类。 即包含 agentmain 方法的类。 此属性是必需的,如果不存在,代理将无法启动。 注:这是类名,而不是文件名或路径。
两种代理模式的启动方式:
1、premain启动代理的方式:
在jvm的启动参数中加入
Java代码
-javaagent:jarpath[=options]
jarpath 是代理 JAR 文件的路径,options 是代理选项。此开关可以在同一代码行使用多次,从而创建多个代理。多个代理可以使用相同的 jarpath。代理 JAR 文件必须遵守 JAR 文件规范。代理类必须实现公共静态premain 方法,该方法的原理与main应用程序入口点类似。在 Java 虚拟机 (JVM) 初始化后,每个 premain 方法将按照指定代理的顺序调用,然后将调用实际的应用程序 main 方法。每个 premain 方法必须按照依次进行的启动顺序返回。
-javaagent使用方法
一个java程序中-javaagent这个参数的个数是没有限制的,所以可以添加任意多个java agent。
所有的java agent会按照你定义的顺序执行。
例如:
Java代码
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代码
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代码
java -javaagent:MyAgent2.jar=thisIsAgentArgs -jar MyProgram.jar
MyAgent2中premain接收到的agentArgs的值将是”thisIsAgentArgs” (不包括双引号)
2、agentmain启动代理的方式:
先通过VirtualMachine.attach(targetVmPid)连接到虚拟机,然后通过virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");注册agent代理类。
Java代码
// 被监控jvm的pid(windows上可以通过任务管理器查看)
String targetVmPid = "5936";
// Attach到被监控的JVM进程上
VirtualMachine virtualmachine = VirtualMachine.attach(targetVmPid);
// 让JVM加载jmx Agent
String javaHome = virtualmachine.getSystemProperties().getProperty("java.home");
String jmxAgent = javaHome + File.separator + "lib" + File.separator + "management-agent.jar";
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
三、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
四、ClassFileTransformer
Java代码
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