前言:
热加载可以在修改完代码后,不重启应用,实现类信息更新,以节省开发时等待启动时间。本文主要从热加载概念、原理、常见框架、实现等角度为你揭开热加载的层层面纱。
但是,美团的远程热部署框架Sonic还区分了本地热部署、远程热部署,但感觉本质上还是属于热加载的范畴。
本地热部署:则是能够在项目运行中感知到特定文件代码的修改而使项目不重新启动就能生效。远程热部署:则是本地代码改变之后,不用重新打包上传服务器重启项目就能生效,本地改变之后能够自动改变服务器上的项目代码。热加载一般基于以下三方面技术实现:
基于JVMTI接口。如 HotSwap,限制是只能修改方法体。JRebel。使用基本无限制,并且能和大部分 IDE 以及框架融合。商用、收费。基于ClassLoader。如 OSGi、Tomcat。针对这种,考虑到 JVM 规范中单个类只能被 load/define 一次的约束,一般实现都是关闭旧的 ClassLoader 并创建新 ClassLoader 来实现。热加载是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。
Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。
方法1:最根本的方式是修改JVM的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件(JRebel和美团Sonic是使用这种方式的,但Sonic是使用了Dcevm)。方法2:创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热加载。首先,需要了解一下 Java 虚拟机现有的加载机制,即双亲委派。系统在使用一个 classloader 来加载类时,会先询问当前 classloader 的父类是否有能力加载,如果父类无法实现加载操作,才会将任务下放到该 classloader 来加载。
这种自上而下的加载方式的好处是,让每个 classloader 执行自己的加载任务,不会重复加载类。但是,这种方式却使加载顺序非常难改变,让自定义 classloader 抢先加载需要监听改变的类成为了一个难点。
虽然,无法抢先加载该类,但是仍然可以用自定义 classloader 创建一个功能相同的类,让每次实例化的对象都指向这个新的类。当这个类的 class 文件发生改变的时候,再次创建一个更新的类,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。
如果需要兼容一些框架进行热加载,需要另外对框架中的文件进行监听并处理相应的重载的逻辑。
Java中的类从被加载到内存中到卸载出内存为止,一共经历了七个阶段:加载、验证、准备、解析、初始化、使用、卸载。 在加载的阶段,虚拟机通过类加载器需要完成以下三件事:
通过一个类的全限定名来获取定义此类的二进制字节流;将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。官方定义的Java类加载器有BootstrapClassLoader、ExtClassLoader、AppClassLoader。这三个类加载器分别负责加载不同路径的类的加载,并形成一个父子结构。
JVM判断两个类对象是否相同的依据:一是类全称;一个是类加载器。
通过修改类名,避免类加载时出现类对象相同的问题(比如,让每次加载的类都保存成一个带有版本信息的 class)。
// 在 class 文件发生改变时重新定义这个类
private Class<?> redefineClass(String className, ClassSource classSource){
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = null;
classSource.update();
String enhancedClassName = classSource.getEnhancedName();
try {
cr = new ClassReader(new FileInputStream(classSource.getFile()));
} catch (IOException e) {
e.printStackTrace();
return null;
}
//EnhancedModifier,这个增强组件的作用是改变原有的类名
EnhancedModifier em = new EnhancedModifier(cw, className.replace(".", "/"),
enhancedClassName.replace(".", "/"));
//ExtendModifier,改变原有类的父类,让这个修改后的派生类能够实现同一个原始类(此时原始类已经转成接口了)
ExtendModifier exm = new ExtendModifier(em, className.replace(".", "/"),
enhancedClassName.replace(".", "/"));
cr.accept(exm, 0);
byte[] code = cw.toByteArray();
classSource.setByteCopy(code);
Class<?> clazz = defineClass(enhancedClassName, code, 0, code.length);
classSource.setClassCopy(clazz);
return clazz;
}
复制
自定义 classloader 还有一个作用是监听会发生改变的 class 文件,classloader 会管理一个定时器,定时依次扫描这些 class 文件是否改变。
(3) 改变创建对象的行为
Java 虚拟机常见的创建对象的方法有两种,一种是静态创建,直接 new 一个对象,一种是动态创建,通过反射的方法,创建对象。 由于已经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创建方法都无法成立。我们要做的是将实例化原始类的行为变成实例化派生类。 对于第一种方法,需要做的是将静态创建,变为通过 classloader 获取 class,然后动态创建该对象。
ITestClass testClass = (ITestClass)MyClassLoader.getInstance().
findClass("com.example.TestClass").newInstance();
复制
对于第二种创建方法,需要通过修改 Class.forName()和 ClassLoader.findClass()的行为,使他们通过自定义加载器加载类。
因此,这里需要用到 ASM 来修改 class 文件了,查找到所有 new 对象的语句,替换成通过 classloader 的形式来获取对象的形式。
@Override
public void visitTypeInsn(int opcode, String type) {
if (opcode==Opcodes.NEW && type.equals(className)) {
List<LocalVariablenode> variables = node.localVariables;
String compileType = null;
for(int i = 0; i < variables.size(); i++){
LocalVariableNode localVariable = variables.get(i);
compileType = formType(localVariable.desc);
if(matchType(compileType) && !valiableIndexUsed[i]) {
valiableIndexUsed[i] = true;
break;
}
}
mv.visitMethodInsn(Opcodes.INVOKESTATIC, CLASSLOAD_TYPE, "getInstance", "()L" + CLASSLOAD_TYPE + ";");
mv.visitLdcInsn(type.replace("/", "."));
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, CLASSLOAD_TYPE, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "newInstance", "()Ljava/lang/Object;");
mv.visitTypeInsn(Opcodes.CHECKCAST, compileType);
flag = true;
} else {
mv.visitTypeInsn(opcode, type);
}
}
复制
(4) 使用 JavaAgent 拦截默认加载器的行为
之前实现的类加载器已经解决了热加载所需要的功能。可是 JVM 启动时,默认加载程序的是AppClassLoader,并不会用自定义的加载器加载 classpath下的所有 class 文件,如果之后用自定义加载器重新加载已经加载的 class,有可能会出现 LinkageError 的 exception。所以必须在应用启动之前,重新替换已经加载的 class。
java.lang.LinkageError
attempted duplicate class definition
复制
如果在 jdk1.4 之前,能使用的方法只有一种,改变 jdk 中 classloader 的加载行为,使它指向自定义加载器的加载行为。 但在 jdk5.0 之后,有了另一种侵略性更小的办法,即 Java Agent 方法,Java Agent 可以在 JVM 启动之后,应用启动之前的短暂间隙,提供空间给用户做一些特殊行为。比较常见的应用,是利用 JavaAgent 做面向切面的编程,在方法间加入监控日志等。利用 JavaAgent替换原始字节码,阻止原始字节码被 Java 虚拟机加载。
编写Agent
public class ReloadAgent {
public static void premain(String agentArgs, Instrumentation inst){
GeneralTransformer trans = new GeneralTransformer();
inst.addTransformer(trans);
}
}
复制
然后,再编写一个 manifest 文件,将 Premain-Class属性设置成定义一个拥有 premain方法的类名即可。 生成一个包含这个 manifest 文件的 jar 包。
manifest-Version: 1.0
Premain-Class: com.example.ReloadAgent
Can-Redefine-Classes: true
复制
在执行应用的参数中增加 -javaagent参数 , 加入这个 jar。这样在执行应用的之前,会优先执行 premain方法中的逻辑,并且预解析需要加载的 class。
(5) 替换 class
虽然,无法抢先加载该类,可以利用 JavaAgent拦截默认加载器,使用自定义 classloader 创建一个功能相同的类,替换默认加载的class文件,让每次实例化的对象都指向这个新的类。 只需要实现 一个 ClassFileTransformer的接口,利用这个实现类完成 class 替换的功能。
@Override
public byte [] transform(ClassLoader paramClassLoader, String paramString,
Class<?> paramClass, ProtectionDomain paramProtectionDomain,
byte [] paramArrayOfByte) throws IllegalClassFormatException {
String className = paramString.replace("/", ".");
if(className.equals("com.example.TestClass")){
MyClassLoader cl = MyClassLoader.getInstance();
cl.redefineClass(className);
return cl.getByteCode(className);
}
return null;
}
复制
其他优化:
不会清理内存,有内存溢出的风险,需要新增对应的方案来处理;需要维护一些配置信息、依赖关系、元数据信息等;......https://www.jianshu.com/p/90f149d6cf95
Java 热加载/热部署技术 · 1907
Onenote怎么添加加载项?-Onenote添加加载项教程