Arthas 热部署实现原理


Arthas 是由阿里巴巴开源实现的一套 Java 诊断工具,能够实现对 Java 进程全方位的诊断与调试。其丰富实用的命令选项使得其深受 Java 开发工程师的喜爱。

在 Arthas 中有一个命令可以实现部分代码的热部署功能,这里介绍一下它具体是怎么实现的。

热部署概念

热部署在近些年来在 Java 组成的应用十分受欢迎。主要源于随着项目的越来越大,Java 程序的启动需要加载大量的内容,导致启动时间十分耗时。对于线上应用程序而言,相较于程序的运行时长,启动时长几乎可以忽略不计。而对于 Java 开发者来说,由于需要对代码进行调试,频繁的启动应用所带来的启动耗时便成了负担。这时候热部署便应运而生了。

代码的热部署表示在在不重启应用的情况下,只将代码修改的部分替换到正在运行的程序中,从而实现动态代码修改的效果。这样就不需要开发者只改一行代码也要重新启动项目了。节省下来的重启时间可以专注于业务代码的开发过程,从而提升开发效率。


热部署方案

目前市面上主流的热部署解决方案有 JRebelHotSwapAgent等,如果开发者使用的是 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接口中体现

image

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 类,通过 虚拟机线程对目标类常量池中对应的符号进行了一一替换:

image


通过 Instrumentation实现热部署的一些限制

并不是所有改动热更新都将会成功,当前使用 Instrumentation#redefineClasses 还是存在一些限制。从源码可以看到,我们只是对字节码中已有的一些内容进行了替换,因此无法添加,删除方法或字段,也不能更改方法的签名或继承关系。

参考资料

Java JVMTI和Instrumention机制介绍
open-jdk
RedefineCommand
手把手教你实现热更新功能,带你了解 Arthas 热更新背后的原理
热更新原理及实践注意


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM