`

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文件

    这篇博客“Java类动态加载(一)——java源文件动态编译为class文件”可能主要探讨了如何在运行时将Java源代码(.java)编译成对应的字节码文件(.class),并将其加载到Java虚拟机(JVM)中。以下是对这个主题的详细解析...

    Java之——类热加载

    Java之——类热加载 在Java编程中,类的加载是程序运行的重要环节。传统的Java应用程序在启动时,由JVM(Java虚拟机)通过类加载器将类加载到内存中,一旦加载完成,除非程序退出,否则这些类通常不会被重新加载。...

    java元数据——CLass类

    Java元数据——Class类 Java中的元数据Class类是一个基础的概念,它代理了这个类的类型信息、方法签名、属性等信息。每个类都有一个Class对象,它用来创建这个类的所有对象。每个对象的创建都依赖于Class对象的创建...

    深入java虚拟机(二)——类的生命周期(上)类的加载和连接1

    【深入Java虚拟机(二)——类的生命周期(上)类的加载和连接】 Java虚拟机(JVM)是Java程序的核心,它负责解释和执行Java字节码。类的生命周期在JVM中是一个关键的概念,它涵盖了从类的加载到卸载的整个过程。...

    深入java虚拟机(六)——类加载的父亲委托机制1

    在Java编程中,类加载器(ClassLoader)扮演着至关重要的角色,它负责将类的.class文件加载到Java虚拟机(JVM)中,使程序得以运行。从JDK 2.0开始,Java引入了一种特殊的类加载策略——父亲委托机制,这一机制保证...

    Java有根儿:Class文件以及类加载器.doc

    类加载器(ClassLoader)是Java程序中的关键组件,负责查找和加载Class文件。Java的类加载机制遵循双亲委派模型,即从顶层的启动类加载器开始,向下逐级加载。这样可以防止类的冲突,并确保核心库的安全性。 在深入...

    2021Java大厂面试题——大厂真题之携程-Java高级.pdf

    在深入理解Java虚拟机(JVM)如何加载Class文件之前,我们需要明确一点:Java的所有类都需要通过类加载器加载到JVM中才能被执行。这个过程对开发者来说通常是透明的,但在一些特殊情况下,例如使用反射时,了解类加载...

    115个Java面试题和答案——终极列表

    - **Class类**:代表运行时的类信息,通过它能动态创建对象,获取类的属性和方法。 - **反射API**:如Method, Field, Constructor等,用于在运行时操作类的成员。 9. **JVM** - **类加载器**:理解双亲委派模型...

    【JVM】类的奇幻漂流——类加载机制探秘

    【JVM】类的奇幻漂流——类加载机制探秘 Java虚拟机(JVM)是运行Java程序的核心组件,它负责将我们编写的类加载到内存中并执行。类加载机制是JVM的一个重要组成部分,它确保了程序的正常运行。本文将带你深入理解...

    Java Class文件反编译工具 jd-gui

    Java Class文件是Java程序编译后的二进制格式,它包含了类和接口的定义、方法体、常量池等信息,但这些信息是以机器可读的字节码形式存在,对于人类来说不易理解。为了查看和理解Class文件内部的源代码,我们就需要...

    Java类加载原理解析

    Java 类加载机制是Java技术体系中的重要组成部分,它负责将类的字节码文件从磁盘、网络或其他数据源加载到Java虚拟机中,然后转化为运行时的数据结构。理解这一机制对于解决程序中出现的如`java.lang....

    Java类加载原理解析文档

    Java类加载机制是Java技术体系中的重要组成部分,它关乎到程序运行时的类查找与实例化。当遇到`java.lang.ClassNotFoundException`这类异常时,往往与类加载有关。本文将详细解析Java类加载原理,分为三篇文章进行...

    深入java虚拟机(三)——类的生命周期(下)类的初始化1

    【深入Java虚拟机(三)——类的生命周期(下)类的初始化1】 类的生命周期在Java中是一个关键的概念,它涵盖了从加载到卸载的整个过程。在类的生命周期中,初始化阶段是非常重要的,因为它涉及到类的静态变量的赋值...

    class——Java高级开发工程师必须懂得

    Java 高级开发工程师在工作中必须掌握的关键知识点之一是关于`Class`...通过这样的专业培训,您将能够更好地理解和掌握Java高级开发中的关键概念,包括`Class`类的使用和动态加载类的技术,从而在职业生涯中取得成功。

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

    - **加载**:类加载是将类的`.class`文件读入内存,并创建一个`java.lang.Class`对象。当使用类时,JVM会经历加载、连接和初始化三个步骤。 - **连接**:连接阶段包括验证、准备和解析。验证确保类的正确性;准备...

    java反射机制 字符串——java对象

    在Java中,反射机制的核心类是`java.lang.Class`,它代表了运行时的类信息。通过Class对象,我们可以获取到类的结构信息,如类名、方法、字段等,并能在运行时动态地创建对象、调用方法。 字符串在Java中扮演着重要...

    博客《 夯实JAVA基本之二 —— 反射(3):类内部信息获取》对应源码

    这篇博客《夯实JAVA基本之二 —— 反射(3):类内部信息获取》深入探讨了如何通过反射机制获取Java类的内部信息。在Android开发中,反射的应用尤为广泛,例如动态加载类、处理注解、适配不同设备等场景。 首先,要...

    2021Java字节跳动面试题——面向字节_JVM(下).pdf

    #### 二、JVM加载Class文件的原理机制 Java类加载过程是由类加载器完成的,具体来说是由`ClassLoader`及其子类实现的。类加载器实质上是把类文件从硬盘读取到内存中。 类加载的方式分为隐式加载和显式加载: - **...

    java基础——————试题库

    - Class类:代表运行时的类信息,可以动态加载类、创建对象、获取方法和字段。 - 构造器、方法、字段的invoke()方法:在运行时调用对象的方法或构造器。 16. **泛型** - 泛型引入了类型参数,提供更强的类型检查...

Global site tag (gtag.js) - Google Analytics