AI生成-JAVA 热更新:动态加载子类热更(ClassLoader 加载新类的方式)
JAVA 热更新:动态加载子类热更(ClassLoader 加载新类的方式)
目前有2种方式:
1.JAVA热更新1:Agent方式热更(jdk提供的agent)
2.JAVA热更新2:动态加载子类热更(classloader加载新类的方式)
- 为什么不能动态加载本类:也能加载,就是使用的时候需要用获取
Object script = scriptManager.getScriptByClassName(PRINT_SCRIPT_CLASS);Object 的形式,调用方法也只能用反射,特别麻烦。因为使用方不能出现本类,如果出现的话这个类就会加载到AppClassLoader,和热更时用自己定义的ClassLoader不是同一个,虽然类名一样但是加载器不一样,这样这两个Class就是不一样的,等使用的时候本类 script = (本类)scriptManager.getScriptByClassName(PRINT_SCRIPT_CLASS),会报错java.lang.ClassCastException。
AI生成-Java 热更新 ClassLoader 核心问题总结 - deyang - 博客园
第一种是jdk自带的agent机制,但不知道是否是jdk8的bug,我们大量服务器热更之后偶尔有个别服务器热更之后会出现虚拟机崩溃的情况,所以我们一般采用的是第二种,而且第二种优点是没有agent的各种限制,不只是热更,还支持不停服动态新增代码。但也有一定弊端,就是得按照框架的写法去实现。
两种方式各有利弊把,我们线上一般使用方式2,如果有方式2不支持的,就用方式1做补充。
| 对比维度 | Agent 方式(字节码增强) | ClassLoader 方式(动态加载) |
|---|---|---|
| 核心原理 | 运行时直接修改已加载类的方法体字节码,类本身不变。 | 创建新的类加载器,重新读取并加载类文件,新旧类在JVM中并存。 |
| 改动范围 | 仅限方法体内部的代码逻辑。 ❌ 不能增删方法/字段,不能改继承/注解。 | 无限制。 ✅ 可随意增删方法、字段、修改继承关系或注解。 |
| 生效时效 | 即时生效,调用retransformClasses后影响后续所有调用。 |
非即时,需等待旧实例消亡,或手动将引用切换到新实例。 |
| 内存风险 | 低。 旧类只被替换,未被卸载,不会产生内存泄漏(Metaspace OOM)。 | 高。 若旧类加载器有引用残留,会引发严重的内存泄漏(Metaspace OOM)。 |
| 类型转换安全 | 安全。 内存中只有一个类定义,不存在类型转换异常(ClassCastException)。 |
极不安全。 新旧类被视作不同类型,对象混用极易触发ClassCastException。 |
| 代码侵入性 | 无。 无需提前设计热更接口,可随时对线上代码“做手术”。 | 极高。 代码必须提前按“模块容器”设计(如门面模式、工厂模式),否则无法实现。 |
| 操作复杂度 | 低。 写一个Agent JAR包,或直接使用Arthas等工具一键热更。 | 极高。 需自行管理类加载器生命周期、依赖版本、对象引用追踪。 |
| 典型场景 | 线上紧急Bug修复(如空指针、计算错误、日志调整)。 开发期配合JRebel免重启。 | 大版本玩法迭代(如重构整个战斗模块)。 自研插件化架构(如动态加载新活动、新英雄)。 |
| 社区/生态 | 生态成熟,有Arthas、BTrace、JRebel等成熟工具支持。 | 几乎没有通用工具,主要依赖OSGi规范或团队自研框架。 |
| 选型建议 | ⭐⭐⭐⭐⭐ 日常运维首选,安全、快速、低成本。 | ⭐⭐ 除非架构设计初期就强制模块化隔离,否则不建议在生产环境使用。 |
一、概述
在 Java 开发中,热更新(Hot Reload / Hot Swap)指的是在不重启 JVM 的情况下,替换或新增运行时的类逻辑。本文介绍一种基于 自定义 ClassLoader 动态加载子类 的方式来实现热更新。
核心思想
- 定义 接口(Interface)或抽象父类 作为契约,业务代码面向接口编程。
- 将具体的实现类放在 外部目录(非 classpath),通过自定义 ClassLoader 加载。
- 替换实现时,创建新的 ClassLoader 实例加载新的
.class文件,旧实例由 GC 回收。 - 实现类本身还是同一个(全限定名不变,.class 文件被重新编译覆盖),但每次热更时创建新的 ClassLoader 实例来加载它。JVM 中类的唯一标识是 全限定名 + ClassLoader,不同的 ClassLoader 实例加载同一个 .class 文件会被 JVM 视为两个不同的类,因此可以做到同名类的热替换。
二、架构设计
┌──────────────────────────────────────┐
│ 调用方 (Invoker) │
│ 持有接口引用,调用接口方法 │
└────────────────┬─────────────────────┘
│ 面向接口编程
▼
┌──────────────────────────────────────┐
│ 接口 (IWorker / BaseTask) │
│ 定义业务方法契约 │
└────────────────┬─────────────────────┘
│ 实现
▼
┌──────────────────────────────────────┐
│ 实现类 v1 (com.demo.WorkerImpl) │
│ 实现类 v2 (com.demo.WorkerImpl) │ ← 不同 ClassLoader 加载
│ 实现类 v3 (com.demo.WorkerImpl) │ 同全限定名,不同实例
└──────────────────────────────────────┘
关键点:不同 ClassLoader 实例加载的同名 .class 文件,在 JVM 中被视为不同的类。因此可以实现"同名类的热替换"。
三、代码实现
3.1 定义业务接口
// IWorker.java —— 放在主 classpath 中,由 AppClassLoader 加载
package com.demo;
public interface IWorker {
String getName();
void doWork(String param);
}
重要:接口必须由 双亲(父 ClassLoader) 加载,这样子 ClassLoader 加载的实现类才能正确转型。
3.2 自定义 ClassLoader
// HotSwapClassLoader.java
package com.demo;
import java.io.*;
public class HotSwapClassLoader extends ClassLoader {
private final String classDir; // .class 文件所在目录
public HotSwapClassLoader(String classDir) {
// 指定父 ClassLoader,保证接口类从父加载器加载
super(ClassLoader.getSystemClassLoader());
this.classDir = classDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 全限定名 → 文件路径
String filePath = classDir + File.separator
+ name.replace('.', File.separatorChar) + ".class";
byte[] classBytes = readClassBytes(filePath);
if (classBytes == null) {
throw new ClassNotFoundException("未找到类文件: " + filePath);
}
// defineClass 将字节数组转为 Class 对象
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("加载类失败: " + name, e);
}
}
private byte[] readClassBytes(String filePath) throws IOException {
File file = new File(filePath);
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
return bos.toByteArray();
}
}
}
3.3 热更新管理器
// HotSwapManager.java
package com.demo;
import java.io.*;
import java.nio.file.*;
import java.util.concurrent.ConcurrentHashMap;
public class HotSwapManager {
// 业务实现类的全限定名
private final String implClassName;
// .class 文件的监控目录
private final String classDir;
// 当前使用的实例
private volatile IWorker currentWorker;
// 当前使用的 ClassLoader
private volatile HotSwapClassLoader currentLoader;
public HotSwapManager(String implClassName, String classDir) {
this.implClassName = implClassName;
this.classDir = classDir;
loadNewInstance();
}
/**
* 创建新的 ClassLoader 并加载新的实现类实例
*/
private synchronized void loadNewInstance() {
try {
// 1. 创建新的 ClassLoader(关键:每次都是新实例)
HotSwapClassLoader newLoader = new HotSwapClassLoader(classDir);
// 2. 加载实现类
Class<?> clazz = newLoader.loadClass(implClassName);
// 3. 反射创建实例
IWorker newWorker = (IWorker) clazz.getDeclaredConstructor().newInstance();
// 4. 原子替换
IWorker oldWorker = this.currentWorker;
this.currentWorker = newWorker;
this.currentLoader = newLoader;
System.out.println("[HotSwap] 热更新成功 → " + newWorker.getName());
// 5. 提示 GC 回收旧的 ClassLoader
if (oldWorker != null) {
oldWorker = null;
System.gc();
}
} catch (Exception e) {
System.err.println("[HotSwap] 热更新失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 获取当前实例(供业务调用)
*/
public IWorker getWorker() {
return currentWorker;
}
/**
* 触发热更新(可由文件监听、定时扫描、HTTP 接口等触发)
*/
public void reload() {
loadNewInstance();
}
// ============ 可选:文件变化自动热更 ============
/**
* 启动文件监听(JDK 7+ WatchService)
*/
public void startWatch() {
new Thread(() -> {
try {
WatchService watchService = FileSystems.getDefault().newWatchService();
Paths.get(classDir).register(watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
String fileName = event.context().toString();
if (fileName.endsWith(".class")) {
System.out.println("[HotSwap] 检测到文件变化: " + fileName);
// 防抖:等待文件写入完成
Thread.sleep(500);
reload();
}
}
key.reset();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "HotSwap-Watcher").start();
}
}
3.4 业务使用示例
// Main.java
package com.demo;
public class Main {
public static void main(String[] args) throws Exception {
// 实现类放在项目根目录下的 hotswap 文件夹
String classDir = System.getProperty("user.dir") + File.separator + "hotswap";
HotSwapManager manager = new HotSwapManager(
"com.demo.WorkerImpl", // 实现类的全限定名
classDir // .class 文件目录
);
// 启动文件监听(可选)
manager.startWatch();
// 模拟持续运行,定期调用业务方法
for (int i = 0; i < 100; i++) {
IWorker worker = manager.getWorker();
worker.doWork("request-" + i);
Thread.sleep(3000);
// 也可通过手动触发(如 HTTP 接口)
// if (某个条件) { manager.reload(); }
}
}
}
3.5 实现类示例(放在外部目录编译)
// WorkerImpl.java —— 编译后将 .class 放到 hotswap/com/demo/ 目录下
package com.demo;
public class WorkerImpl implements IWorker {
private final String version = "v1.0"; // 修改此处后重新编译 → 热更
@Override
public String getName() {
return "Worker-" + version;
}
@Override
public void doWork(String param) {
System.out.println("[" + version + "] 处理请求: " + param);
// ... 业务逻辑 ...
}
}
3.6 HTTP 触发热更新(Spring Boot 场景)
// HotSwapController.java
@RestController
@RequestMapping("/hotswap")
public class HotSwapController {
@Resource
private HotSwapManager hotSwapManager;
@PostMapping("/reload")
public String reload() {
hotSwapManager.reload();
IWorker worker = hotSwapManager.getWorker();
return "热更新完成,当前版本: " + worker.getName();
}
@GetMapping("/version")
public String version() {
return hotSwapManager.getWorker().getName();
}
}
四、编译与部署流程
4.1 目录结构
project/
├── src/main/java/com/demo/
│ ├── IWorker.java ← 接口(随主项目编译)
│ ├── HotSwapClassLoader.java
│ ├── HotSwapManager.java
│ └── Main.java
├── hotswap/ ← 外部热更目录
│ └── com/demo/
│ └── WorkerImpl.class ← 实现类编译产物放这里
└── hotswap-src/ ← 实现类源码(开发用)
└── com/demo/
└── WorkerImpl.java
4.2 编译热更实现类
# 将实现类编译到 hotswap 目录
javac -d ./hotswap ./hotswap-src/com/demo/WorkerImpl.java
# 修改源码后重新编译 → 自动/手动触发热更
4.3 注意事项
| 项目 | 说明 |
|---|---|
| 接口稳定性 | 接口必须保持兼容,否则 (IWorker) newInstance 转型会抛出 ClassCastException |
| 同名类隔离 | 不同 ClassLoader 加载的同名类互不感知,无法直接类型转换(所以必须用接口) |
| 静态变量 | 新的 ClassLoader 会创建全新的静态变量副本,旧数据不保留 |
| 内存泄漏 | 旧的 ClassLoader 如果没有被 GC,会导致 Metaspace 泄漏。确保没有外部强引用指向旧类的实例或 Class 对象 |
| 线程安全 | 替换实例时使用 volatile 保证可见性,使用 synchronized 保证加载过程的原子性 |
五、原理分析
5.1 ClassLoader 双亲委派模型
Bootstrap ClassLoader (加载 rt.jar)
↑
Extension ClassLoader (加载 jre/lib/ext)
↑
Application ClassLoader (加载 classpath)
↑
HotSwapClassLoader #1 ──→ 加载 WorkerImpl (版本A)
HotSwapClassLoader #2 ──→ 加载 WorkerImpl (版本B) ← 同名但不同类
IWorker接口由 AppClassLoader 加载(唯一实例)WorkerImpl由各自的 HotSwapClassLoader 实例加载(每次热更创建新实例)- 转型
(IWorker) obj使用的是 AppClassLoader 中的IWorker,因此转型成功
5.2 为什么不同 ClassLoader 的同名类被视为不同类
JVM 中类的唯一标识是:全限定名 + 加载它的 ClassLoader 实例。
ClassA (loaded by Loader#1) ≠ ClassA (loaded by Loader#2)
这就是热替换的根本原理。
5.3 类卸载条件
旧的 ClassLoader 被 GC 回收的条件:
- 该 ClassLoader 加载的所有类的 所有实例 都已不可达
- 该 ClassLoader 加载的所有类的 Class 对象 都已不可达
- 该 ClassLoader 对象本身不可达
5.4 使用 -XX:+TraceClassUnloading 观察类卸载
java -XX:+TraceClassLoading -XX:+TraceClassUnloading Main
六、适用场景与局限
适用场景
| 场景 | 说明 |
|---|---|
| 规则引擎 | 业务规则频繁变化,热更规则实现类 |
| 脚本化插件 | 插件式架构,动态加载/卸载功能模块 |
| 线上紧急修复 | 不停机修复线上 Bug |
| 策略模式热更 | 动态替换算法/策略实现 |
| A/B 测试 | 动态切换不同版本的实现 |
不适用场景
| 局限 | 说明 |
|---|---|
| 修改接口签名 | 接口变更会导致转型失败,需要重启 |
| 新增接口方法 | 需要在接口层新增,无法热更接口本身 |
| 框架 Bean 管理 | Spring 等框架管理的 Bean 需要额外处理 |
| 复杂依赖 | 实现类依赖的其他类如果变更,也需要热更 |
七、进阶方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| ClassLoader 热更(本文) | 新 ClassLoader 加载新类 | 简单可控 | 仅支持接口/父类定义的契约 |
| Instrumentation + Agent | java.lang.instrument 修改字节码 |
可修改任意类的任意方法 | 实现复杂,不能改字段/方法签名 |
| OSGi | 模块化框架,Bundle 级别热更 | 成熟的企业级方案 | 学习成本高,侵入性强 |
| Groovy / JVM 脚本语言 | 脚本引擎动态执行 | 极简,适合规则脚本 | 性能低于原生 Java |
| JPDA (HotSwap) | IDE Debug 模式的 HotSwap | 开发期方便 | 仅限方法体修改,生产不可用 |
八、总结
基于自定义 ClassLoader 动态加载子类的热更新方案,核心是利用 不同 ClassLoader 实例加载的同名类互不冲突 这一 JVM 特性,通过 接口隔离 实现新旧版本的平滑切换。
核心步骤:
- 定义接口 → 2. 实现类放外部目录 → 3. 自定义 ClassLoader 加载 → 4. 新 ClassLoader 替换旧实现 → 5. 旧 ClassLoader 被 GC 回收
该方案适合对 可替换性要求高、接口稳定 的业务场景,是 Java 热更新中最经典、最可控的方式之一。
浙公网安备 33010602011771号