Java 热更新的目的即在不停止正在运行系统的情况下, 对类进行升级替换.
需要注意的是热更新只能修改方法内部逻辑,临时变量等
不能添加,删除方法,不能修改方法的参数, 也不能更改方法的签名或继承关系。
前置知识
如果想要研究如何替换类, 那么首先应该知道java对于类的加载是如何进行的
Java类的加载过程
在Java语言里,编译时并不进行链接工作,类型的加载、链接和初始化工作都是在Java虚拟机执行过程中进行的。在JVM启动过程中(或者JVM运行中),外部class字节码文件(或者合法的二进制流文件)会经过一系列的过程转化为JVM中执行的数据。这一系列的过程我们称为
从类被JVM加载到内存开始,到卸载出内存为止,整个生命周期包括:加载、链接、初始化、使用和卸载五个过程.
加载(重要)
加载就是jvm将二进制文件(class文件就是最常用的)加载到内存中的步骤, 十分重要, 这是你的代码能被jvm使用的关键步骤.
分为以下几步:
-
(可以是jvm自带的类加载器, 也可以是用户自定义的类加载器)(具体加载哪些二进制文件由类加载器指定, 通过指定路径的方式, 限定了加载范围)
-
将二进制流 ( 文件 ) 内的常量池内容(class常量池)转化到方法区的运行时常量池;
-
在堆内存中创建一个表示该类的 java.lang.Class<?> 对象,作为访问该类的入口。也就是Class对象.
每个被创建的对象,都能通过 Object.class 来获得对这个类的唯一标识的引用。
当对象上调用一个方法时,JVM会得到该实例堆Class对象,再通过Class对象调用具体的方法。
也就是说,假设 mo 是 MyObject 类的一个对象,当你调用 mo.method()时,
JVM 实际会进行类似下面这样的调用: mo.getClass().getDeclaredMethod("method").invoke(mo)
(虚拟机实现并不会这样写,但是最终的结果是一致的)
类加载器的简介,原理以及意义可以参加我的另一篇文章
链接
链接过程包括验证,准备,解析三个过程, 这部分和本文关系不大, 感兴趣的可以自行了解
初始化
初始化阶段主要完成两个方法clinit和init来初始化变量和资源。
clinit代表类构造器, 由编译器自动生成
init代表构造函数, 就是你写java类的时候的构造函数
初始化阶段后, 用户就可以使用类来创建对象了
卸载(重要)
一个类是否被卸载和类的生命周期紧密相关
类的生命周期
当类完成初始化后,它的生命周期就开始了。
当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载。
由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时不再被引用
类的引用关系
类和类加载器
类加载器内部有一个java集合, 存放了所有自己加载的java类
类对象总是引用类加载器, 可以通过调用Class对象的getClassLoader()方法,获得它的类加载器。
实例和类
一个类的实例总是持有对于类的引用
类何时会被卸载
由实例, 类, 类加载器之间的引用关系, 可以得到如下结论
热更新的难点
学习完前置知识后, 我们来思考下如果我们要实现热更新的话, 应该怎么实现.
容易做到的部分
-
修改完java代码后, 编译得到一个class文件 (注意哪些能修改, 哪些不能修改)
-
替换项目中对应的class文件
步骤1,2 如果实现自动化可能有一些难度, 但是如果是实验的话, 很好做到, 因此不是本文研究的重点.
不容易的部分
假设修改前的类为A类, 修改后的类为B类. (B类和A类的包路径+类名是一样的)
因此根据类的加载过程, 用户自定义的类会被应用程序类加载器(jvm自带的)加载, 即A类已经被加载并且在方法区的常量池内有该类, 那么程序在运行的过程中, 如果需要使用到该类, 会直接使用常量池的内容(即A类), 即使项目中的class文件已经是B类了.
每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。 不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
根据这个逻辑, 那么即使修改了class文件, 也没有办法实现热更新, jvm根本就不会去加载新的class文件.
同时根据卸载类的要求, 应用程序类加载器加载的类无法被卸载, 因此也不可能通过卸载一个旧类来加载新类来实现
自定义类加载器实现热更新
既然应用程序类加载器加载的类无法实现热更新, 那么如果我提前设计好哪些类需要被热更新, 然后这些类在使用的过程中, 通过自定义的类加载器来加载.
需要热更新的时候, 代码重新创建一个类加载器, 通过新的类加载器加载新的类, 在使用的时候, 通过新的类加载器来获得类, 此时得到的类就是更新后的类.
为什么一定要通过类加载器获得类, 才是热更后的类?
正常的new对象 当虚拟机遇到一条new指令时候
-
首先去检查这个指令的参数是否能 在常量池中能否定位到一个类的符号引用 (即类的带路径全名),并且检查这个符号引用代表的类是否已被加载、解析和初始化过
-
因为热更的类被应用类加载器加载过, 因此new命中的都是之前加载的类对象 都是旧代码
因此不能使用new来获得热更后的对象, 同时不能使用Class.forName(String name) (这个方法就是new的时候默认使用的)
只能使用Class.forName(String name, boolean initialize,ClassLoader loader) 来指定类加载器使用
自定义类加载器demo
工程结构如下:
A,B,C 需要热更新的类
package hotRrefrsh.hotswapclass;
public class A {
public void hello() {
System.out.println("A hello");
}
}
package hotRrefrsh.hotswapclass;
public class B {
public void hello() {
System.out.println("B hello");
}
}
package hotRrefrsh.hotswapclass;
public class C {
public void hello() {
System.out.println("C hello");
}
}
工程类HotSwapClassFactory
package hotRrefrsh.hotswapclass;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
// 通过该工程类获得热更新后的object
public class HotSwapClassFactory {
// 运行中程序使用的自定义类加载器 项目启动时 加载一次目标类
private static ClassLoader classLoader;
// 记录类全名和类文件修改的时间
private static Map<String, Long> classNameToLastModifiedMap = new HashMap<>();
static {
System.out.println("自定义类加载器开始初始化");
createNewClassLoader();
System.out.println("初始化加载完毕");
}
public static void checkHotSwap() {
System.out.println("探测一次");
final Boolean[] swapFlag = {Boolean.FALSE};
// 遍历map 判断类文件是否发生修改
classNameToLastModifiedMap.forEach( (className, time) -> {
String path = classLoader.getResource("").getFile();
String fileName = className.replace(".", "/") + ".class";
if (isClassModified(path + fileName, className)) {
System.out.println("类文件发生改变");
swapFlag[0] = Boolean.TRUE;
}
});
// 通过标志位判断类加载器是否需要被替换
if (swapFlag[0]) {
System.out.println("类文件被修改, 采用新的类加载器替换旧类加载器");
createNewClassLoader();
}
}
//判断是否被修改过
private static boolean isClassModified(String classFilename, String className) {
boolean returnValue = false;
File file = new File(classFilename);
if (file.lastModified() > classNameToLastModifiedMap.get(className)) {
returnValue = true;
}
return returnValue;
}
private static void createNewClassLoader() {
System.out.println("创建新的类加载器");
classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
try {
if (name.startsWith("hotRrefrsh.hotswapclass")) {
// 记录指定路径下的类名和时间
classNameToLastModifiedMap.put(name, System.currentTimeMillis());
// 注意当前类和期望修改的类要在同一包下 下述两行代码才对,否则需要修改路径的问题
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}
// 其它的类 由双亲委派处理 不会被当前自定义类加载器加载
return super.loadClass(name);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
// 初始化加载指定路径下的类
try {
// 这里是手动加载 实际应用中可以通过反射得到某个包路径下所有类名
// 执行loadClass才会将指定class文件加载
classLoader.loadClass("hotRrefrsh.hotswapclass.A");
classLoader.loadClass("hotRrefrsh.hotswapclass.B");
classLoader.loadClass("hotRrefrsh.hotswapclass.C");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Object getHotSwapObjectByClassName(String name)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 已经被加载过的类 无法通过classLoader.loadClass的方式获取
// 只能通过Class.forName(方法获取)
Class clazz = Class.forName(name, Boolean.FALSE, classLoader);
return clazz.newInstance();
}
}
手动编译 生成修改后class文件的类 ReCompile
package hotRrefrsh.hotswapclass;
class ReCompile {
public static void main(String[] args) {
new A().hello();
new B().hello();
new C().hello();
}
}
模拟代码一直运行的类 AlwaysRun
package hotRrefrsh;
import hotRrefrsh.hotswapclass.HotSwapClassFactory;
import java.lang.reflect.InvocationTargetException;
public class AlwaysRun {
public static void main(String [] args)
throws InterruptedException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
// 开启一个线程 循环探测A, B, C类是否发生修改 如果发生修改, 创建新的类加载器替换原来的类加载器
// 实际环境中通过定时任务来循环探测
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
HotSwapClassFactory.checkHotSwap();
}
}
};
new Thread(runnable).start();
while (true) {
Object a = HotSwapClassFactory.getHotSwapObjectByClassName("hotRrefrsh.hotswapclass.A");
a.getClass().getMethod("hello", new Class[]{}).invoke(a);
Object b = HotSwapClassFactory.getHotSwapObjectByClassName("hotRrefrsh.hotswapclass.B");
b.getClass().getMethod("hello", new Class[]{}).invoke(b);
Object c = HotSwapClassFactory.getHotSwapObjectByClassName("hotRrefrsh.hotswapclass.C");
c.getClass().getMethod("hello", new Class[]{}).invoke(c);
Thread.sleep(20000);
}
}
}
实验流程
-
运行AlwaysRun的main方法 自定义类加载器被初始化 加载了相应的类, 探测线程持续探测中
-
修改A类和B类中 方法的返回值
package hotRrefrsh.hotswapclass;
public class A {
public void hello() {
System.out.println("A hello rrrr");
}
}
package hotRrefrsh.hotswapclass;
public class B {
public void hello() {
System.out.println("B hello wwww");
}
}
-
执行ReCompile类的main方法 将修改后的类编译生成对应的class文件
-
探测线程探测到文件发生变化
-
热更新成功