什么是热加载
- 热加载是指在不重启服务的情况下让更改的代码生效。
- 热加载基于 Java 的类加载器实现,可以显著提升开发以及调试的效率。
- 由于热加载的不安全性,一般不会用于正式的生产环境。
热加载与热部署的区别
相同点:
- 都可以在不重启服务的情况下编译/部署项目。
- 都是基于 Java 的类加载器实现的。
不同点:
- 部署方式上:
- 热部署是在服务器运行时重新部署项目。
- 热加载是在运行时重新加载 class。
- 实现原理上:
- 热部署是直接重新加载整个应用,耗时相对较高。
- 热加载是在运行时重新加载 class,后台启动一个线程不断检测类是否改变。
- 使用场景上:
- 热部署更多的是在生产环境使用。
- 热加载更多在开发环境上使用。线上由于安全性问题不会使用,难以监控。
类加载五个阶段
类的完整生命周期一共是7个阶段,除图里最后的使用(Using)和卸载(Unloading)外的五个阶段是类加载阶段。
简单描述一下类加载的五个阶段:
- 加载阶段:找到类的静态存储结构,加载到虚拟机,定义数据结构。用户可以自定义类加载器。
- 验证阶段:确保字节码是安全的,确保不会对虚拟机的安全造成危害。
- 准备阶段:确定内存布局,确定内存遍历,赋初始值(注意:是初始值,也有特殊情况)。
- 解析阶段:将符号变成直接引用。
- 初始化阶段:调用程序自定义的代码。规定有且仅有5种情况必须进行初始化:
- new(实例化对象)、getstatic(获取类变量的值,被final修饰的除外,他的值在编译器时放到了常量池)、putstatic(给类变量赋值)、invokestatic(调用静态方法) 时会初始化。
- 调用子类的时候,发现父类还没有初始化,则父类需要立即初始化。
- 虚拟机启动,用户要执行的主类,主类需要立即初始化,如 main 方法。
- 使用 java.lang.reflect包的方法对类进行反射调用方法是会初始化。
- 当使用JDK 1.7的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、 REF_putStatic、 REF_invokeStatic的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化。
要说明的是,类加载的 5 个阶段中,只有加载阶段是用户可以自定义处理的,而验证阶段、准备阶段、解析阶段、初始化阶段都是用 JVM 来处理的。
实现类的热加载
实现思路
根据分析:
- Java 程序在运行的时候,首先会把 class 类文件加载到 JVM 中,而类的加载过程又有五个阶段,五个阶段中只有加载阶段用户可以进行自定义处理。
- 所以我们如果能在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的 class 文件,然后重新进行加载的话,那么理论上就可以实现一个简单的 Java 热加载。
可以得出实现思路:
- 实现自己的类加载器。
- 从自己的类加载器中加载要热加载的类。
- 不断轮训要热加载的类 class 文件是否有更新。
- 如果有更新,重新加载。
自定义类加载器
设计 Java 虚拟机的团队把类的加载阶段放到的 JVM 的外部实现(通过一个类的全限定名来获取描述此类的二进制字节流 ),这样就可以让程序自己决定如何获取到类信息。而实现这个加载动作的代码模块,我们就称之为 “类加载器”。在 Java 中,类加载器也就是 ClassLoader。
所以如果我们想要自己实现一个类加载器,就需要继承 ClassLoader 然后重写里面 findClass的方法,同时因为类加载器是双亲委派模型实现(也就说,除了一个最顶层的类加载器之外,每个类加载器都要有父加载器,而加载时,会先询问父加载器能否加载,如果父加载器不能加载,则会自己尝试加载)所以我们还需要指定父加载器。
1 | package com.justxzm.alex; |
定义要类型热加载的类
假设某个接口(BaseTask.java)下的某个方法(execute)要进行热加载处理。
首先定义接口信息:
1 | package com.justxzm.alex; |
写一个这个接口的实现类:
1 | package com.justxzm.alex; |
后面要让这个类可以通过MyClassLoader进行自定义加载。为了避免无意义的重复加载,类的热加载应当只有在类的信息被更改然后重新编译之后进行重新加载,所以需要判断class是否进行了更新,需要记录class类的修改时间,以及对应的类信息。
编译一个类用来记录某个类对应的某个类加载器以及上次加载的class的修改时间:
1 | package com.justxzm.alex; |
热加载获取类信息
每次调用要热加载的类时,我们都要进行检查类是否被更新然后决定要不要重新加载。可以使用一个简单的工厂模式进行封装。
要注意是加载class文件需要指定完整的路径,所以类中定义了CLASS_PATH 常量。
1 | package com.justxzm.alex; |
热加载测试
写一个线程不断的检测要热加载的类是不是已经更改需要重新加载,然后运行测试:
1 | package com.justxzm.alex; |
主线程:
1 | package com.justxzm.alex; |
启动后看到控制台不断的输出:
1 | init |
这时候我们随便改下MyTask类的execute方法的输出内容然后保存:
1 | public class MyTask implements BaseTask { |
可以看到控制台的输出已经自动更改了 :
1 | init |