Skip to content

双亲委派机制破坏/热替换

双亲委派:JVM 的安全基石

前面我们学了全盘委托——类加载时先委托给父加载器,只有父无法完成时才自己加载。这套机制叫「双亲委派模型」(Parent Delegation Model)。

但有些场景下,双亲委派需要被打破。

双亲委派的实现

标准实现

ClassLoader 的 loadClass() 方法是双亲委派的标准实现:

java
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

java
// 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。

java
// 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 等容器需要支持热部署——同一应用的新版本类需要替换旧版本。

java
// 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()

java
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

java
// 使用 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. 旧类加载器及其加载的类没有引用后,可以被卸载

常见的热替换实现

技术原理
OSGiBundle 类加载器隔离 + 动态更新
Tomcat 热部署WebappClassLoader 隔离 + 重新创建
Arthas 热更新RedefineClasses / RetransformClasses
DCEVMJVM 层面的类替换

Arthas 的热更新

bash
# Arthas 热更新步骤
$jad --source-only com.example.User > /tmp/User.java
# 编辑 /tmp/User.java
$mc /tmp/User.java
$retransform /tmp/User.class

Arthas 使用了 Instrumentation API 提供的类重新定义功能。

本节小结

双亲委派与打破双亲委派的核心要点:

维度说明
双亲委派类加载时先委托给父加载器,父无法完成才自己加载
loadClass()双亲委派的实现核心方法
安全性防止核心类被篡改,保证类统一性
打破场景JDBC SPI、TCCL、热部署、OSGi
打破方式自定义 ClassLoader、TCCL、OSGi 框架
热替换创建新类加载器,卸载旧类加载器

理解双亲委派及其打破方式,才能理解 JDBC、SPI、热部署等重要技术的设计原理。

下一节,我们来看 Java9 类加载新特性

基于 VitePress 构建