Arthas 之热更新原理,并实现简易版热更新功能
热更新原理
Arthas 热更新功能看起来很神奇,实际上离不开 JDK 一些 API,分别为 instrument API 与 attach API。
Instrumentation
Java Instrumentation 是 JDK5 之后提供接口。使用这组接口,我们可以获取到正在运行 JVM 相关信息,使用这些信息我们构建相关监控程序检测 JVM。另外, 最重要我们可以替换和修改类的,这样就实现了热更新。
Instrumentation 存在两种使用方式,一种为 pre-main
方式,这种方式需要在虚拟机参数指定 Instrumentation 程序,然后程序启动之前将会完成修改或替换类。使用方式如下:
java -javaagent:jar Instrumentation_jar -jar xxx.jar
这种方式只能在应用启动之前生效,存在一定的局限性。
JDK6 针对这种情况作出了改进,增加 agent-main
方式。我们可以在应用启动之后,再运行 Instrumentation
程序。启动之后,只有连接上相应的应用,我们才能做出相应改动,这里我们就需要使用 Java 提供 attach API。
Attach API
Attach API 位于 tools.jar 包,可以用来连接目标 JVM。Attach API 非常简单,内部只有两个主要的类,VirtualMachine
与 VirtualMachineDescriptor
。
VirtualMachine
代表一个 JVM 实例, 使用它提供 attach
方法,我们就可以连接上目标 JVM。
VirtualMachine vm = VirtualMachine.attach(pid);
VirtualMachineDescriptor
则是一个描述虚拟机的容器类,通过该实例我们可以获取到 JVM PID (进程 ID), 该实例主要通过 VirtualMachine#list
方法获取。
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()){
System.out.println(descriptor.id());
}
介绍完热更新涉及的相关原理,接下去使用上面 API 实现热更新功能。
实现热更新功能
实现 agent-main
首先需要编写一个类,包含以下两个方法:
public static void agentmain (String agentArgs, Instrumentation inst); [1]
public static void agentmain (String agentArgs); [2]
上面的方法只需要实现一个即可。若两个都实现, [1] 优先级大于 [2],将会被优先执行。
接着读取外部传入 class 文件,调用 Instrumentation#redefineClasses
,这个方法将会使用新 class 替换当前正在运行的 class,这样我们就完成了类的修改。
com.agent.AgentMain
的代码如下:
package com.agent;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import org.objectweb.asm.ClassReader;
public class AgentMain {
/**
* @param agentArgs 外部传入的参数,类似于 main 函数 args
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
// 从 agentArgs 获取外部参数
System.out.println("start agentmain.");
// 这里将会传入 class 文件路径
String path = agentArgs;
try {
// 读取 class 文件字节码
RandomAccessFile f = new RandomAccessFile(path, "r");
final byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
// 使用 asm 框架获取类名
final String clazzName = readClassName(bytes);
// inst.getAllLoadedClasses 方法将会获取所有已加载的 class
for (Class clazz : inst.getAllLoadedClasses()) {
// 匹配需要替换 class
if (clazz.getName().equals(clazzName)) {
ClassDefinition definition = new ClassDefinition(clazz, bytes);
// 使用指定的 class 替换当前系统正在使用 class
inst.redefineClasses(definition);
}
}
} catch (Exception e) {
System.err.println("agentmain error.");
}
}
/**
* 使用 asm 读取类名
* @param bytes
* @return
*/
private static String readClassName(final byte[] bytes) {
return new ClassReader(bytes).getClassName().replace("/", ".");
}
}
其中,ClassReader
类需要引入 Jar 包:
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
配置 MANIFEST.MF
完成代码之后,我们还需要往 jar 包 MANIFEST.MF
写入以下属性。
## 指定 agent-main 全名
Agent-Class: com.agent.AgentMain
## 设置权限,默认为 false,没有权限替换
classCan-Redefine-Classes: true
使用 maven-assembly-plugin
,将上面的属性写入文件中:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<finalName>hotswap-jdk</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Agent-Class>com.agent.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
<manifest>
<mainClass>com.main.JvmAttachMain</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
使用 Attach API
以上完成热更新主要代码,接着使用 Attach API,连接目标虚拟机,触发热更新的代码。
在这个启动类,我们最终调用 VirtualMachine#loadAgent
,JVM 将会使用上面 AgentMain 方法使用传入 class 文件替换正在运行 class。
com.main.JvmAttachMain
的代码如下:
package com.main;
import java.io.File;
import com.sun.tools.attach.VirtualMachine;
public class JvmAttachMain {
public static void main(String[] args){
String pid = "41556";
String classPath = "E:\\RedefineSuccess.class";
// 获取 Agent jar 路径
String jarPath = System.getProperty("user.dir") + File.separator + "target" + File.separator + "hotswap-jdk.jar";
System.out.println("this redefine jar path:" + jarPath);
try {
VirtualMachine vm = VirtualMachine.attach(pid); // 待绑定的jvm进程的pid号
// 运行最终 AgentMain 中方法
vm.loadAgent(jarPath,classPath);
} catch (Throwable e) {
System.err.println("ERROR:" + e.getMessage());
e.printStackTrace();
}
}
}
运行
- 将
com.agent.AgentMain
打包成hotswap-jdk.jar
- 运行测试类 RedefineSuccess.java
- 通过
jps
获取到进程 ID - 将进程 ID 和反编译生成的 class 文件传入
com.main.JvmAttachMain
- 运行
com.main.JvmAttachMain
相关问题
编译引入 tools.jar
由于 Attach API 位于 tools.jar 中,而在 JDK8 之前 tools.jar 与我们常用 JDK jar 包并不在同一个位置,所以编译与运行过程可能找不到该 jar 包,从而导致报错。
则需要 Maven 引入:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>${java.version}</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
程序运行中 tools.jar
运行程序时抛出 java.lang.NoClassDefFoundError
,主要原因还是系统未找到 tools.jar 导致。
在运行参数加入 -Xbootclasspath/a:${java_home}/lib/tools.jar
,完整运行命令如下:
java -Xbootclasspath/a:${java_home}\lib\tools.jar -jar hotswap-jdk.jar 22132 E:\RedefineSuccess.class