快盘下载:好资源、好软件、快快下载吧!

快盘排行|快盘最新

当前位置:首页软件教程电脑软件教程 → 热加载原理解析与实现

热加载原理解析与实现

时间:2022-09-26 13:10:17人气:作者:快盘下载我要评论


发布于2022-06-19 21:14:51阅读5270

前言:

热加载可以在修改完代码后,不重启应用,实现类信息更新,以节省开发时等待启动时间。本文主要从热加载概念、原理、常见框架、实现等角度为你揭开热加载的层层面纱。

一. 热部署与热加载

概念:

热部署(Hot Deploy):热部署针对的是​​容器​​或者是整个应用,包括运行需要使用到的各种文件(jar包、JS、CSS、html、配置文件),新的资源或者修改了一些代码,需要在不停机的情况下的重新加载整个应用;热加载(Hot Swap):热加载针对的是单个字节码文件,指的是重新编译后, 不需要停机 ,应用程序就可以加载使用新的class文件。

区别:

热部署:针对整个应用,包括Jar包、class文件、配置文件等;会清空内存;热加载;热加载:一般只针对class文件(或者针对框架自定义一些重载逻辑);一般不会清空内存,有内存溢出风险;

但是,美团的远程热部署框架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 文件发生改变的时候,再次创建一个更新的类,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。

如果需要兼容一些框架进行热加载,需要另外对框架中的文件进行监听并处理相应的重载的逻辑。

ClassLoader类加载(双亲委派模型)

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

相关文章

网友评论

快盘下载暂未开通留言功能。

关于我们| 广告联络| 联系我们| 网站帮助| 免责声明| 软件发布

Copyright 2019-2029 【快快下载吧】 版权所有 快快下载吧 | 豫ICP备10006759号公安备案:41010502004165

声明: 快快下载吧上的所有软件和资料来源于互联网,仅供学习和研究使用,请测试后自行销毁,如有侵犯你版权的,请来信指出,本站将立即改正。