文章目录

  • 什么是热加载
  • 热加载 VS 热部署
  • 部署方式
  • 实现原理
  • 使用场景
  • 准备
  • Java类加载机制
  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 如何实现热加载
  • 自定义类加载器
  • 为啥需要自定义类加载器
  • 如何自定义类加载器
  • 定时监控类修改
  • 优化
  • 解决方案


什么是热加载

热加载是指可以在不重启服务的情况下让更改的代码生效

热加载可以显著的提升开发以及调试的效率,它是基于 Java 的类加载器实现的,但是由于***热加载的不安全性,一般不会用于正式的生产环境***。

热加载 VS 热部署

热加载和热部署,都可以在不重启服务的情况下编译/部署项目,都是基于 Java 的类加载器实现的。

部署方式

  • 热部署在运行时重新部署整个项目;
  • 热加载在运行时重新加载class;

实现原理

  • 热部署是在运行时重新部署整个应用,耗时相对较高;
  • 热加载是在运行时重新加载class,后台启动一个线程检测类的改变;

使用场景

  • 热部署通常在生产环境使用;
  • 热加载通常在开发环境使用,线上由于安全性问题不会使用,难以监控

准备

Java类加载机制

Java类生命周期:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载,前5个为类加载阶段。

类加载的 5 个阶段中,只有加载阶段是用户可以自定义处理的,而验证阶段、准备阶段、解析阶段、初始化阶段都是 JVM 来处理的。

java 热加载类 java 模块化 热加载_类加载器

加载

加载阶段,JVM做3件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

官方定义的类加载器有3个,通过双亲委派机制确定加载顺序:

java 热加载类 java 模块化 热加载_java 热加载类_02

类加载器

加载路径

BootstrapClassLoader

处于类加载器层次结构的最高层,负责 sun.boot.class.path 路径下类的加载,默认为 jre/lib 目录下的核心 API 或 -Xbootclasspath 选项指定的 jar 包

ExtClassLoader

加载路径为 java.ext.dirs,默认为 jre/lib/ext 目录或者 -Djava.ext.dirs 指定目录下的 jar 包加载

AppClassLoader

加载路径为 java.class.path,默认为环境变量 CLASSPATH 中设定的值。也可以通过 -classpath 进行指定

默认情况下,使用关键字new或者Class.forName都是通过AppClassLoader类加载器来加载的

默认情况下如果要加载一个类,会优先将此类交给其父类进行加载(直到顶层的BootstrapClassLoader),如果父类都无法加载,那么才会将此类交给子类加载。

java 热加载类 java 模块化 热加载_源码_03

验证

确保字节码是安全的,确保不会对虚拟机的安全造成危害

准备

确定内存布局,确定内存遍历,赋初始值(注意:是初始值,也有特殊情况)

解析

将符号引用变成直接引用。

初始化

调用程序自定义的代码。规定有且仅有5种情况必须进行初始化:

  1. new(实例化对象)、getstatic(获取类变量的值,被final修饰的除外,他的值在编译器时放到了常量池)、putstatic(给类变量赋值)、invokestatic(调用静态方法) 时会初始化;
  2. 调用子类的时候,发现父类还没有初始化,则父类需要立即初始化;
  3. 虚拟机启动,用户要执行的主类,主类需要立即初始化,如 main 方法;
  4. 使用 java.lang.reflect包的方法对类进行反射调用方法,会初始化;
  5. 当使用JDK 1.7的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、 REF_putStatic、 REF_invokeStatic的方法句柄, 且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化;

如何实现热加载

自定义类加载器

需要自定义类加载器。

为啥需要自定义类加载器

为啥需要自定义类加载器,为什么不能使用AppClassLoader或者ExtClassLoader实现热加载呢?

ClassLoader源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        //确认该类是否已加载,JVM是以 类加载器实例 + 类文件 来表示同一个类
        //若这个类被不同类加载器加载,findLoadedClass也还是返回未加载
        //所以后面需要每次重新生成类加载器实例
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

可以看到,Class<?> c = findLoadedClass(name)一旦发现类已经加载,便不会重新加载,如此便不能实现热加载。

如何自定义类加载器

继承ClassLoader,默认loadClass遵循双亲委派机制,因此重写ClassLoader

public class CustomClassLoader extends ClassLoader{

    private static final String CLASS_FILE_SUFFIX = ".class";

    //AppClassLoader的父类加载器
    private ClassLoader extClassLoader;

    public CustomClassLoader(){
        ClassLoader j = String.class.getClassLoader();
        if (j == null) {
            j = getSystemClassLoader();
            while (j.getParent() != null) {
                j = j.getParent();
            }
        }
        this.extClassLoader = j ;
    }

    protected Class<?> loadClass(String name, boolean resolve){

        Class cls = null;
        cls = findLoadedClass(name);
        if (cls != null){
            return cls;
        }
        //获取ExtClassLoader
        ClassLoader extClassLoader = getExtClassLoader();
        try {
            //确保自定义的类不会覆盖Java的核心类
            //因为对于classpath上的类,ExtClassLoader是无法加载的,
            //但是如果这里换成AppClassLoader,它能加载classpath上的类,
            //显然会优先加载,也就轮不到自定义加载器了,就变成不同的双亲委派加载机制
            cls = extClassLoader.loadClass(name);
            if (cls != null){
                return cls;
            }
        }catch (ClassNotFoundException e ){

        }
        cls = findClass(name);
        return cls;
    }

    @Override
    public Class<?> findClass(String name) {
        byte[] bt = loadClassData(name);
        return defineClass(name, bt, 0, bt.length);
    }

    private byte[] loadClassData(String className) {
        // 读取Class文件呢
        InputStream is = getClass().getClassLoader().getResourceAsStream(className.replace(".", "/")+CLASS_FILE_SUFFIX);
        ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
        // 写入byteStream
        int len =0;
        try {
            while((len=is.read())!=-1){
                byteSt.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 转换为数组
        return byteSt.toByteArray();
    }

    public ClassLoader getExtClassLoader(){
        return extClassLoader;
    }
}

为什么要先获取ExtClassLoader类加载器呢?其实这里是借鉴了Tomcat里面的设计,是为了避免自定义的类加载器覆盖了一些核心类。例如java.lang.Object。

为什么是获取ExtClassLoader类加载器而不是获取AppClassLoader呢?这是因为如果获取了AppClassLoader进行加载,AppClassLoader加载器能加载classpath上的所有类,自然也包括测试类,也就轮不到自定义加载器了,那么不还是双亲委派的规则了嘛。

定时监控类修改

使用ScheduledThreadPoolExecutor来进行周期性的监控文件是否修改。

在程序启动的时候记录文件的最后修改时间。随后周期性的查看文件的最后修改时间是否改动。如果改动了就重新生成类加载器进行替换。

测试类:

public class Test {

    public void test() {
        System.out.println("Test.test(). Version 1");
    }
}

定时任务类:

public class WatchDog implements Runnable{

    private Map<String,FileDefine> fileDefineMap;

    public WatchDog(Map<String,FileDefine> fileDefineMap){
        this.fileDefineMap = fileDefineMap;
    }

    @Override
    public void run() {
        File file = new File(FileDefine.WATCH_PACKAGE);
        File[] files = file.listFiles();
        for (File watchFile : files){
            long newTime = watchFile.lastModified();
            FileDefine fileDefine = fileDefineMap.get(watchFile.getName());
            long oldTime = fileDefine.getLastDefine();
            //如果文件被修改了,那么重新生成累加载器加载新文件
            if (newTime!=oldTime){
                fileDefine.setLastDefine(newTime);
                loadMyClass();
            }
        }
    }

    public void loadMyClass(){
        try {
            //重新生成类加载器,重新加载类
            CustomClassLoader customClassLoader = new CustomClassLoader();
            Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
            //不能使用Test test = (Test) cls.newInstance()
            //因为加载两个类的类加载器不同
            Object test = cls.newInstance();
            Method method = cls.getMethod("test");
            method.invoke(test);
        }catch (Exception e){
            System.out.println(e);
        }
    }
}

优化

在Java中确定两个类是否相等,除了看他们两个类文件是否相同以外还会看他们的类加载器是否相同

所以即使是同一个类文件,如果是两个不同的类加载器来加载的,那么它们的类型就是不同的。

WatchDog类是由new出来的,所以默认是AppClassLoader来加载的,而test变量的声明类型是WatchDog方法中的一个属性,所以也是由AppClassLoader来加载的,因此两个类不相同,所以不能直接使用Test test = (Test) cls.newInstance(),会报ClassCastException类类型转换异常。

解决方案

通过接口。

默认情况下,如果实现了一个接口,那么此接口一般都是以子类的加载器为主的

即如果没有特殊要求的话,例如A implements B 如果A的加载器是自定义的。那么B接口的加载器也是和子类是一样的。

所以可以通过改造自定义加载器让AppClassLoader类加载器来加载接口。

if ("com.example.watchfile.ITest".equals(name)){
    try {
        cls = getSystemClassLoader().loadClass(name);
    } catch (ClassNotFoundException e) {

    }
    return cls;
}

创建接口:

public interface ITest {

    void test();
}

这样两边的类加载器就相同了:

CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
ITest test = (ITest) cls.newInstance();
test.test();

参考:牛逼!自己手写一个热加载~