双亲委派机制破坏/热替换
双亲委派:JVM 的安全基石
前面我们学了全盘委托——类加载时先委托给父加载器,只有父无法完成时才自己加载。这套机制叫「双亲委派模型」(Parent Delegation Model)。
但有些场景下,双亲委派需要被打破。
双亲委派的实现
标准实现
ClassLoader 的 loadClass() 方法是双亲委派的标准实现:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 先检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委托给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 没有父加载器,尝试 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器找不到
}
// 4. 父加载器找不到,自己加载
if (c == null) {
c = findClass(name);
}
}
return c;
}流程:
1. 检查是否已加载(findLoadedClass)
2. 委托给父加载器(parent.loadClass)
3. 父加载器找不到,自己加载(findClass)双亲委派的层级
loadClass 的委派链:
AppClassLoader.loadClass()
│
▼
ExtClassLoader.loadClass()
│
▼
BootstrapClassLoader.loadClass() // 最终,如果都找不到当类加载请求沿着链向上传递,Bootstrap ClassLoader 是链的终点。
双亲委派的意义
1. 安全性:防止核心类被篡改
场景:尝试加载 java.lang.String
AppClassLoader.loadClass("java.lang.String")
│
▼
ExtClassLoader.loadClass("java.lang.String")
│
▼
BootstrapClassLoader.loadClass("java.lang.String")
│
▼
加载成功,返回 Bootstrap 的 String即使有人在 classpath 中放一个假的 java.lang.String,也不会被加载——因为父加载器会先找到真正的 String。
2. 类的统一性:避免同一个类被多次加载
不同类加载器加载的类在运行时是隔离的,通过全盘委托保证核心类只有一份。
需要打破双亲委派的场景
场景一:JDBC——驱动加载问题
JDBC 是最早打破双亲委派的场景。
问题
JDBC 的接口在 java.sql 包中(Bootstrap ClassLoader 加载),但驱动实现在 classpath 中(AppClassLoader 加载)。
核心接口:java.sql.Driver(Bootstrap 加载)
驱动实现:com.mysql.jdbc.Driver(App 加载)如果走双亲委派,DriverManager 无法加载驱动实现——因为它本身是 Bootstrap 加载的,它看到的都是 Bootstrap 的类。
解决方案:Thread Context ClassLoader
// DriverManager 的源码(简化)
public class DriverManager {
static {
// 使用 TCCL 加载驱动
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
for (String driverClass : drivers) {
Class<?> cls = Class.forName(driverClass, true, tccl);
// 注册驱动...
}
}
}TCCL 默认是 AppClassLoader,所以能正确加载驱动。
场景二:SPI(Service Provider Interface)
SPI 是 JDK 提供的一种服务发现机制,如 JNDI、JAXP、JDBI 等都基于 SPI。
// SPI 的工作原理
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
for (Driver driver : loader) {
driver.connect(...);
}ServiceLoader.load() 内部使用 TCCL 来加载服务实现。
SPI 的双亲委派困境
接口:java.sql.Driver(Bootstrap)
实现:com.example.MyDriver(App)
问题:Bootstrap 的 ServiceLoader 看不到 App 的实现!解决方案:ServiceLoader 使用 TCCL 来加载实现类。
场景三:热部署
Tomcat、OSGi 等容器需要支持热部署——同一应用的新版本类需要替换旧版本。
// Tomcat 的热部署原理
// 每个 Web 应用有自己的类加载器
// 更新时,销毁旧类加载器,创建新类加载器
// 旧类加载器的类随之卸载Tomcat 打破双亲委派:
标准双亲委派:
AppClassLoader.loadClass() → ExtClassLoader → Bootstrap
Tomcat 的 WebappClassLoader:
1. 先自己加载(findLoadedClass + findClass)
2. 再委托父加载器(不走标准委派链)场景四:OSGi 类加载框架
OSGi 中,每个 Bundle 有自己的类加载器,遵循「类加载器优先」原则:
OSGi 的类加载顺序:
1. Bundle 自己的类
2. Import-Package 的类
3. Require-Bundle 的类
4. Bootstrap 类打破双亲委派的方式
方式一:自定义 ClassLoader,重写 loadClass()
public class HotDeployClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 检查是否是系统类(由父加载器加载)
if (name.startsWith("java.") || name.startsWith("javax.")) {
return getParent().loadClass(name, resolve);
}
// 自己加载(不走标准双亲委派)
return findClass(name);
}
}方式二:Thread Context ClassLoader
// 使用 TCCL 打破双亲委派
ClassLoader oldTCL = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(myClassLoader);
// 这里可以加载 myClassLoader 负责的类
} finally {
Thread.currentThread().setContextClassLoader(oldTCL);
}方式三:OSGi 类加载框架
OSGi 通过自定义类加载策略,打破了标准的双亲委派模型。
热替换(Hot Replacement)
概念
热替换是指在应用运行时,用新版本的类替换旧版本的类,而无需重启 JVM。
实现原理
热替换的实现方式:
1. 创建新的类加载器加载新版本的类
2. 让新类加载器加载的类替换旧版本类
3. 旧类加载器及其加载的类没有引用后,可以被卸载常见的热替换实现
| 技术 | 原理 |
|---|---|
| OSGi | Bundle 类加载器隔离 + 动态更新 |
| Tomcat 热部署 | WebappClassLoader 隔离 + 重新创建 |
| Arthas 热更新 | RedefineClasses / RetransformClasses |
| DCEVM | JVM 层面的类替换 |
Arthas 的热更新
# Arthas 热更新步骤
$jad --source-only com.example.User > /tmp/User.java
# 编辑 /tmp/User.java
$mc /tmp/User.java
$retransform /tmp/User.classArthas 使用了 Instrumentation API 提供的类重新定义功能。
本节小结
双亲委派与打破双亲委派的核心要点:
| 维度 | 说明 |
|---|---|
| 双亲委派 | 类加载时先委托给父加载器,父无法完成才自己加载 |
| loadClass() | 双亲委派的实现核心方法 |
| 安全性 | 防止核心类被篡改,保证类统一性 |
| 打破场景 | JDBC SPI、TCCL、热部署、OSGi |
| 打破方式 | 自定义 ClassLoader、TCCL、OSGi 框架 |
| 热替换 | 创建新类加载器,卸载旧类加载器 |
理解双亲委派及其打破方式,才能理解 JDBC、SPI、热部署等重要技术的设计原理。
下一节,我们来看 Java9 类加载新特性。
