`

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这几个类的用法。
一、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代理服务。
// 被监控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文件,文件内容如下:
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方式必须实现下面两个方法中的其中一个:
public static void premain(String agentArgs, Instrumentation inst);[1]
public static void premain(String agentArgs);[2]

JVM 首先尝试在代理类上调用以下方法:
public static void premain(String agentArgs, Instrumentation inst);

如果代理类没有实现此方法,那么 JVM 将尝试调用:
public static void premain(String agentArgs);



2、通过Attach去启动Agent类方式必须实现下面两个方法中的其中一个:
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 首先尝试对代理类调用以下方法:
public static void agentmain(String agentArgs, Instrumentation inst);

如果代理类没有实现此方法,那么 JVM 将尝试调用:
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文件内容如下:
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的启动参数中加入
-javaagent:jarpath[=options]

jarpath 是代理 JAR 文件的路径,options 是代理选项。此开关可以在同一代码行使用多次,从而创建多个代理。多个代理可以使用相同的 jarpath。代理 JAR 文件必须遵守 JAR 文件规范。代理类必须实现公共静态premain 方法,该方法的原理与main应用程序入口点类似。在 Java 虚拟机 (JVM) 初始化后,每个 premain 方法将按照指定代理的顺序调用,然后将调用实际的应用程序 main 方法。每个 premain 方法必须按照依次进行的启动顺序返回。

-javaagent使用方法
一个java程序中-javaagent这个参数的个数是没有限制的,所以可以添加任意多个java agent。
所有的java agent会按照你定义的顺序执行。
例如:
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 -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 -javaagent:MyAgent2.jar=thisIsAgentArgs -jar MyProgram.jar

MyAgent2中premain接收到的agentArgs的值将是”thisIsAgentArgs” (不包括双引号)

2、agentmain启动代理的方式:
先通过VirtualMachine.attach(targetVmPid)连接到虚拟机,然后通过virtualmachine.loadAgent(jmxAgent, "com.sun.management.jmxremote");注册agent代理类。
// 被监控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
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
分享到:
评论
3 楼 liuInsect 2013-11-19  
sswh 写道
如果类已经初始化,retransformClasses不知道会不会导致类静态初始化块重新执行。
redefineClasses也是同样的疑惑。
如果会重新执行“类静态初始化块”,执行这两个操作危险性很大,不小心会导致虚拟机崩溃的吧?


另外,你提到的:
引用
retransformClasses:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明


好像Eclipse的调试模式下,修改代码也是这样的效果。难道也是调用了retransformClasses来实现代码的热替换的(HotReplace)?



redefineClasses 是不会的,我用idea使用hotswap的时候 就没有执行static块和构造函数的方法。
至于是怎么替换掉的 我也不明白。。
2 楼 sswh 2013-03-19  
如果类已经初始化,retransformClasses不知道会不会导致类静态初始化块重新执行。
redefineClasses也是同样的疑惑。
如果会重新执行“类静态初始化块”,执行这两个操作危险性很大,不小心会导致虚拟机崩溃的吧?


另外,你提到的:
引用
retransformClasses:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明


好像Eclipse的调试模式下,修改代码也是这样的效果。难道也是调用了retransformClasses来实现代码的热替换的(HotReplace)?
1 楼 qiandongbo 2012-11-22  
牛……昨天还在追jmx.agent在哪被加载的……现在懂了

相关推荐

    Java类动态加载(一)——java源文件动态编译为class文件

    NULL 博文链接:https://zheng12tian.iteye.com/blog/1488813

    Java之——类热加载

    java 的类动态加载实现,入口在MsgHandler 通过修改MyManager类中的输出值可用动态编译对应的class,然后替换后查看效果。

    Java反射机制——类的加载方法,创建对象,获取方法以及结构

    加载完类之后,在堆内的方法区中就产生了一个Class类型的对象(一个类只有一个class对象),这个对象就包含了完整类的结构信息,我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构...

    java深度历险——王森

    关于JDK有两个问题是很容易一直困扰Java程序员的地方:一个是CLASSPATH的问题,其实从原理上来说,是要搞清楚JRE的ClassLoader是如何加载Class的;另一个问题是package和import问题,如何来寻找类的路径问题。把这两...

    JVM学习笔记(一)——类的加载机制

    ​ 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 ...

    java源码包---java 源码 大量 实例

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    Java语言程序设计的课程设计项目——利用Java实现地球系动画完整实现实例(第3部分).pdf

    (3)Java 中的动态数组——其实为 Java 中的集合类 为什么要应用静态数组而不使用 Java 中的集合类(如 ArrayList 等)产生动态数组?主要 是考虑到效率和类型两方面的问题。 1) 效率: 要想保存和随机访问一系列...

    JAVA上百实例源码以及开源项目

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    【JVM】类加载器与双亲委派模型

    类加载器在加载阶段,会将class文件加载进方法区。有关类加载的全过程,可以先参考我的另外一篇文章类的奇幻漂流——类加载机制探秘 类加载器的类型 类加载器有以下种类: 启动类加载器(Bootstrap ClassLoader) ...

    浅谈Java类的加载

    早就打算写一篇关于类加载的博客,因为我也曾被...Java文件编译成字节码(.class)文件后,需要通过加载这一重要环节进入虚拟机中 基本环节: 加载–链接–初始化 Loading(加载) 规范中有这样一句话:There are two kind

    Java虚拟机之类加载机制

    后在运行的时候,虚拟机把描述类的信息从class文件加载到内存,然后再进行校验、解析和初始化等过程,后形成可以被java虚拟机“读懂”的java类型。那么从class——&gt;java虚拟机能“读懂”的java类型是本文要讲解的...

    java加壳的问题

    以前我认为这是不可能的,因为动态加载代码这样的内存级别的操作,java无法做到,除非使用JNI(Java Native Interface),调用自己编写的C代码,在C代码中实现动态加载java代码。但是,C如何加载java代码呢?这需要对...

    JAVA上百实例源码以及开源项目源代码

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    java源码包4

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...

    java源码包3

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...

    java源码包2

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...

    JVM解毒——类加载子系统

    带着问题,尤其是面试问题的学习才是最高效的。...Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制

    Java典型模块

    3.3.1 反射的基石——Class类 3.3.2 反射的基本应用 3.3.3 反射的高级应用 3.4 标注 3.4.1 标注的简单使用 3.4.2 JDK的内置标注 3.5 泛型 3.5.1 为什么要使用泛型 3.5.2 泛型的一些特性 3.5.3 泛型的通配符 3.6 类...

    成百上千个Java 源码DEMO 4(1-4是独立压缩包)

    Java二进制IO类与文件复制操作实例 16个目标文件 内容索引:Java源码,初学实例,二进制,文件复制 Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系...

    品优购-面试宝典

    Java源程序(.java文件)——&gt;java字节码文件(.class文件)——&gt;由解释执行器(java.exe)将字节码文件加载到java虚拟机(jvm)——&gt;字节码文件(.class)就会在java虚拟机中执行。

Global site tag (gtag.js) - Google Analytics