Arthas 是由阿里巴巴开源实现的一套 Java 诊断工具,能够实现对 Java 进程全方位的诊断与调试。其丰富实用的命令选项使得其深受 Java 开发工程师的喜爱。
在 Arthas 中有一个命令可以实现部分代码的热部署功能,这里介绍一下它具体是怎么实现的。
热部署概念
热部署在近些年来在 Java 组成的应用十分受欢迎。主要源于随着项目的越来越大,Java 程序的启动需要加载大量的内容,导致启动时间十分耗时。对于线上应用程序而言,相较于程序的运行时长,启动时长几乎可以忽略不计。而对于 Java 开发者来说,由于需要对代码进行调试,频繁的启动应用所带来的启动耗时便成了负担。这时候热部署便应运而生了。
代码的热部署表示在在不重启应用的情况下,只将代码修改的部分替换到正在运行的程序中,从而实现动态代码修改的效果。这样就不需要开发者只改一行代码也要重新启动项目了。节省下来的重启时间可以专注于业务代码的开发过程,从而提升开发效率。
热部署方案
目前市面上主流的热部署解决方案有 JRebel,HotSwapAgent等,如果开发者使用的是 Spirng Boot 的话可以试试 spring-boot-devtools。
今天要介绍的是 Arthas 中的热部署命令redefine,对于热部署解决方案,Arthas 使用了另一种解决方案实现,这里就要讲下 JVMTI 了。
JVMTI 介绍
JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本。
JVMTI可以用来开发并监控虚拟机,可以查看JVM内部的状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。
Arthas 的热部署的实现就是使用了 JVMTI 中的 Instrument
接口,通过该接口可以实现字节码的动态替换。
Instrumention
支持的功能都在java.lang.instrument.Instrumentation
接口中体现
public interface Instrumentation {
//添加一个ClassFileTransformer
//之后类加载时都会经过这个ClassFileTransformer转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
//移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
//将一些已经加载过的类重新拿出来经过注册好的ClassFileTransformer转换
//retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 判断是否支持重新定义类
boolean isRedefineClassesSupported();
//重新定义某个类
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
// 获取全部已经加载的类
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
// 获取对象大小
long getObjectSize(Object objectToSize);
// 添加到启动类加载器搜索路径中
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
// 添加到系统类夹杂器搜索路径中
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
关键词 JVMTI
我们通过addTransformer方法注册了一个ClassFileTransformer,后面类加载的时候都会经过这个Transformer处理。对于已加载过的类,可以调用retransformClasses来重新触发这个Transformer的转换。
redefineClasses 和 retransformClasses 的区别:
transform是对类的byte流进行读取转换的过程,需要先获取类的byte流然后做修改。而redefineClasses更简单粗暴一些,它需要直接给出新的类byte流,然后替换旧的。
transform可以添加很多个,retransformClasses
可以让指定的类重新经过这些transform做转换。
有了上面的工具,Arthas 就拥有了代码热部署的能力了,我们翻下 arthas 的源码看看它是怎么做的:
// 以下代码存在删减,如需
// 创建一个新的热部署模型
RedefineModel redefineModel = new RedefineModel();
// 获取 JVMTI 中的 Instrumentation 对象,来执行 redefineClasses 实现热部署
Instrumentation inst = process.session().getInstrumentation();
// 读取文件路径
// ...
List<ClassDefinition> definitions = new ArrayList<ClassDefinition>();
// Instrumentation inst 获取全部加载的类
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if (bytesMap.containsKey(clazz.getName())) {
// 如果包含则执行热部署
if (hashCode == null && classLoaderClass != null) {
// 获取匹配的类加载器
List<ClassLoader> matchedClassLoaders = ClassLoaderUtils.getClassLoaderByClassName(inst, classLoaderClass);
if (matchedClassLoaders.size() == 1) {
// 设置hashcode
hashCode = Integer.toHexString(matchedClassLoaders.get(0).hashCode());
} else if (matchedClassLoaders.size() > 1) {
// 如果匹配多个,则抛出异常
//...
} else {
process.end(-1, "Can not find classloader by class name: " + classLoaderClass + ".");
return;
}
}
// 获取对应的类加载器
ClassLoader classLoader = clazz.getClassLoader();
// 类定义列表
definitions.add(new ClassDefinition(clazz, bytesMap.get(clazz.getName())));
redefineModel.addRedefineClass(clazz.getName());
}
}
try {
// 获取 JVMTI 中的 Instrumentation 对象,来执行 redefineClasses 实现热部署
inst.redefineClasses(definitions.toArray(new ClassDefinition[0]));
process.appendResult(redefineModel);
process.end();
} catch (Throwable e) {
String message = "redefine error! " + e.toString();
logger.error(message, e);
process.end(-1, message);
}
而 Instrumentation
又是怎么实现热部署的呢?通过翻阅 Instrumentation
的实现我们可以看到 redefineClasses
等方法的实现都是通过 JNI
实现的,因此就需要我们去翻阅一下 JDK 源码了。到目前为止,我们可以得到 Arthas 实现热部署是通过 JDK 的工具类 Instrumentation
实现的,通过传入对应的类对象,和修改后的字节码,便可以实现对目标类的字节码进行调整替换了。
Instrumentation
通过调用 src\hotspot\share\prims\jvmtiRedefineClasses.cpp中的 VM_RedefineClasses
类,通过 虚拟机线程对目标类常量池中对应的符号进行了一一替换:
通过 Instrumentation
实现热部署的一些限制
并不是所有改动热更新都将会成功,当前使用 Instrumentation#redefineClasses 还是存在一些限制。从源码可以看到,我们只是对字节码中已有的一些内容进行了替换,因此无法添加,删除方法或字段,也不能更改方法的签名或继承关系。
参考资料
Java JVMTI和Instrumention机制介绍
open-jdk
RedefineCommand
手把手教你实现热更新功能,带你了解 Arthas 热更新背后的原理
热更新原理及实践注意