Java 热更新 | 七日打卡

1,619 阅读9分钟

Java 热更新的目的即在不停止正在运行系统的情况下, 对类进行升级替换.

需要注意的是热更新只能修改方法内部逻辑,临时变量等

不能添加,删除方法,不能修改方法的参数, 也不能更改方法的签名或继承关系。

前置知识

如果想要研究如何替换类, 那么首先应该知道java对于类的加载是如何进行的

Java类的加载过程

在Java语言里,编译时并不进行链接工作,类型的加载、链接和初始化工作都是在Java虚拟机执行过程中进行的。在JVM启动过程中(或者JVM运行中),外部class字节码文件(或者合法的二进制流文件)会经过一系列的过程转化为JVM中执行的数据。这一系列的过程我们称为类加载过程\color{red}{类加载过程}

从类被JVM加载到内存开始,到卸载出内存为止,整个生命周期包括:加载、链接、初始化、使用和卸载五个过程.

加载(重要)

加载就是jvm将二进制文件(class文件就是最常用的)加载到内存中的步骤, 十分重要, 这是你的代码能被jvm使用的关键步骤.

分为以下几步:

  1. 使用类加载器\color{red}{使用类加载器}(可以是jvm自带的类加载器, 也可以是用户自定义的类加载器)加载二进制流文件\color{red}{加载二进制流文件}(具体加载哪些二进制文件由类加载器指定, 通过指定路径的方式, 限定了加载范围)

  2. 将二进制流 ( 文件 ) 内的常量池内容(class常量池)转化到方法区的运行时常量池;

  3. 在堆内存中创建一个表示该类的 java.lang.Class<?> 对象,作为访问该类的入口。也就是Class对象.

步骤3的意义非常重大,是对象和类关联的原因\color{green}{步骤3的意义非常重大, 是对象和类关联的原因}

每个被创建的对象,都能通过 Object.class 来获得对这个类的唯一标识的引用。
当对象上调用一个方法时,JVM会得到该实例堆Class对象,再通过Class对象调用具体的方法。
也就是说,假设 mo 是 MyObject 类的一个对象,当你调用 mo.method()时, 
JVM 实际会进行类似下面这样的调用: mo.getClass().getDeclaredMethod("method").invoke(mo) 
(虚拟机实现并不会这样写,但是最终的结果是一致的)

类加载器的简介,原理以及意义可以参加我的另一篇文章

链接

链接过程包括验证,准备,解析三个过程, 这部分和本文关系不大, 感兴趣的可以自行了解

初始化

初始化阶段主要完成两个方法clinit和init来初始化变量和资源。

clinit代表类构造器, 由编译器自动生成

init代表构造函数, 就是你写java类的时候的构造函数

初始化阶段后, 用户就可以使用类来创建对象了

卸载(重要)

一个类是否被卸载和类的生命周期紧密相关

类的生命周期

当类完成初始化后,它的生命周期就开始了。

  当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载。

  由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时不再被引用

类的引用关系

类和类加载器

类和类加载器是双向引用的关系\color{red}{类和类加载器是双向引用的关系}

类加载器内部有一个java集合, 存放了所有自己加载的java类

类对象总是引用类加载器, 可以通过调用Class对象的getClassLoader()方法,获得它的类加载器。
实例和类

一个类的实例总是持有对于类的引用

类何时会被卸载

由实例, 类, 类加载器之间的引用关系, 可以得到如下结论

  1. jvm自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。\color{red}{由jvm自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。}

  2. 由用户自定义的类加载器加载的类是可能被卸载的。\color{red}{由用户自定义的类加载器加载的类是可能被卸载的。}

热更新的难点

学习完前置知识后, 我们来思考下如果我们要实现热更新的话, 应该怎么实现.

容易做到的部分

  1. 修改完java代码后, 编译得到一个class文件 (注意哪些能修改, 哪些不能修改)

  2. 替换项目中对应的class文件

步骤1,2 如果实现自动化可能有一些难度, 但是如果是实验的话, 很好做到, 因此不是本文研究的重点.

不容易的部分

假设修改前的类为A类, 修改后的类为B类. (B类和A类的包路径+类名是一样的)

因此根据类的加载过程, 用户自定义的类会被应用程序类加载器(jvm自带的)加载, 即A类已经被加载并且在方法区的常量池内有该类, 那么程序在运行的过程中, 如果需要使用到该类, 会直接使用常量池的内容(即A类), 即使项目中的class文件已经是B类了.

每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。 不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。

根据这个逻辑, 那么即使修改了class文件, 也没有办法实现热更新, jvm根本就不会去加载新的class文件.

同时根据卸载类的要求, 应用程序类加载器加载的类无法被卸载, 因此也不可能通过卸载一个旧类来加载新类来实现

自定义类加载器实现热更新

既然应用程序类加载器加载的类无法实现热更新, 那么如果我提前设计好哪些类需要被热更新, 然后这些类在使用的过程中, 通过自定义的类加载器来加载.

需要热更新的时候, 代码重新创建一个类加载器, 通过新的类加载器加载新的类, 在使用的时候, 通过新的类加载器来获得类, 此时得到的类就是更新后的类.

注意:一定要通过自定义类加载器来获取类,此时的类才是更新后的类\color{red}{注意:一定要通过自定义类加载器来获取类, 此时的类才是更新后的类}

为什么一定要通过类加载器获得类, 才是热更后的类?

正常的new对象 当虚拟机遇到一条new指令时候

  1. 首先去检查这个指令的参数是否能 在常量池中能否定位到一个类的符号引用 (即类的带路径全名),并且检查这个符号引用代表的类是否已被加载、解析和初始化过

  2. 因为热更的类被应用类加载器加载过, 因此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);
        }


    }

}

实验流程

  1. 运行AlwaysRun的main方法 自定义类加载器被初始化 加载了相应的类, 探测线程持续探测中

  2. 修改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");
    }

}
  1. 执行ReCompile类的main方法 将修改后的类编译生成对应的class文件

  2. 探测线程探测到文件发生变化

  3. 热更新成功

参考链接

Java class 热更新:关于对象,类,类加载器

Java动态追踪技术探究

Java 类的热替换 —— 概念、设计与实现

深入探索 Java 热部署

JVM性能优化--类加载器,手动实现类的热加载