Skip to content

双亲委派机制(原理/优势/沙箱安全)

双亲委派:JVM 类加载的「宪法」

双亲委派模型(Parent Delegation Model)是 JVM 类加载子系统的核心设计。你可以把它理解为 JVM 的「宪法」——规定了类加载器之间如何协作,以及为什么这样规定。

理解双亲委派,不只是为了面试,更是为了理解 Java 安全体系、Tomcat 热部署、SPI 机制等核心技术的底层原理。

原理:标准流程

当一个 ClassLoader 收到加载请求时,它不会立即尝试加载,而是先把这个请求「委托」给自己的父加载器:

应用类加载器收到加载请求

        │  "这个类我没见过,你能加载吗?"

扩展类加载器

        │  "这个类我没见过,你能加载吗?"

引导类加载器(Bootstrap)

        │  查找 JAVA_HOME/lib/rt.jar 等

找到:返回 Class 对象
找不到:返回 null,通知子加载器加载


扩展类加载器收到 null,尝试自己加载
找不到:返回 ClassNotFoundException


应用类加载器收到异常,尝试自己加载
找到:defineClass → 返回 Class 对象
找不到:抛出 ClassNotFoundException

源码级理解

ClassLoader.loadClass() 的标准实现完美体现了这个流程:

java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 第一步:检查是否已被当前加载器加载
    Class<?> c = findLoadedClass(name);
    if (c != null) {
        return c;
    }

    // 第二步:尝试让父加载器加载
    try {
        ClassLoader parent = getParent();
        if (parent != null) {
            // 递归调用父加载器的 loadClass
            c = parent.loadClass(name, false);
        } else {
            // 没有父加载器,尝试 Bootstrap
            c = findBootstrapClassOrNull(name);
        }
    } catch (ClassNotFoundException e) {
        // 父加载器找不到,不处理
    }

    // 第三步:父加载器找不到,由当前加载器自己加载
    if (c == null) {
        c = findClass(name);
    }

    // 第四步:是否触发解析
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

这就是双亲委派的全部秘密:第二行的 findLoadedClass 检查 + 第二步的 parent.loadClass 递归调用

三个核心优势

1. 安全性:防止核心类库被篡改

这是双亲委派最重要的价值。

想象一个场景:如果没有双亲委派,恶意代码可以写一个 java.lang.String,植入后门,然后通过 java -cp . 运行:

java
// 恶意代码:java/lang/String.java
package java.lang;

public class String {
    public String() {}

    // 在构造函数里偷偷发送用户的密码
    public String(String s) {
        super();
        sendToServer(s);  // 恶意行为
    }
}

有了双亲委派,这个攻击不可能成功:

用户代码 → Application ClassLoader

Extension ClassLoader(父)

Bootstrap ClassLoader(最顶层)

Bootstrap 加载的是 rt.jar 中的 String

用户写的 java.lang.String 永远不会被加载

所有对 java.lang 包下类的引用,最终都会路由到 Bootstrap 加载的原始类。

2. 类的唯一性保证

双亲委派保证了同一个类不会被两个不同的类加载器重复加载

因为「是否已加载」的检查发生在委派的根节点——如果父加载器已经加载了,子加载器收到的是同一个 Class 对象。

java
public class Uniqueness {
    public static void main(String[] args) throws Exception {
        ClassLoader appLoader = Uniqueness.class.getClassLoader();
        ClassLoader extLoader = appLoader.getParent();
        ClassLoader bootLoader = extLoader.getParent();

        // Object 类只被 Bootstrap 加载一次
        Class<?> obj1 = Object.class;
        Class<?> obj2 = bootLoader.loadClass("java.lang.Object");
        Class<?> obj3 = extLoader.loadClass("java.lang.Object");
        Class<?> obj4 = appLoader.loadClass("java.lang.Object");

        // 四个引用指向同一个 Class 对象
        System.out.println(obj1 == obj2);  // true
        System.out.println(obj2 == obj3);  // true
        System.out.println(obj3 == obj4);  // true
    }
}

3. 类的层次关系一致性

子类加载器依赖父加载器加载的类,保证了在同一个类加载器命名空间内,上层类加载器的类对下层可见,但下层的类对上层不可见。

这在容器隔离(如 Tomcat)中至关重要:Tomcat 的 WebappClassLoader 需要能用自己的类覆盖应用类,但同时能安全地访问 JDK 核心类。

沙箱安全:更深一层

Java 的沙箱安全模型(Security Manager)依赖双亲委派构建多层防护:

┌─────────────────────────────────────────────┐
│         沙箱安全(Security Manager)         │
│  权限检查、代码签名、安全策略文件            │
├─────────────────────────────────────────────┤
│         双亲委派(ClassLoader 层级)        │
│  Bootstrap → Extension → Application        │
│  保证核心类不被篡改                          │
├─────────────────────────────────────────────┤
│         字节码验证器(Bytecode Verifier)   │
│  检查 Class 文件格式和安全操作                │
├─────────────────────────────────────────────┤
│         类型系统(Type Safety)              │
│  JVM 不允许把整数当指针用                    │
└─────────────────────────────────────────────┘

双亲委派是第一道防线:即使字节码验证器被绕过,恶意代码也无法替换 java.lang 等核心包中的类。

双亲委派的例外:线程上下文类加载器

双亲委派不是铁板一块。有些场景必须打破它——这就是线程上下文类加载器(Thread Context ClassLoader)

为什么要打破

JDBC 是最经典的例子:

java
// java.sql.DriverManager 由 Bootstrap 加载(因为它在 rt.jar 中)
// 但 MySQL 的 com.mysql.cj.jdbc.Driver 由 Application ClassLoader 加载
// 如果严格按照双亲委派,Bootstrap 看不到 Application 的类
// DriverManager 就找不到 MySQL 的驱动

解决方案:线程上下文类加载器

java
// 在 DriverManager 加载驱动时:
// Thread.currentThread().getContextClassLoader() 获取线程的上下文加载器
// 用这个加载器去加载 Driver 实现类

// SPI 的标准实现:
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class,
    Thread.currentThread().getContextClassLoader());

线程上下文类加载器的默认值

java
public class ContextLoader {
    public static void main(String[] args) {
        // main 线程的上下文类加载器默认是 Application ClassLoader
        ClassLoader tcl = Thread.currentThread().getContextClassLoader();
        System.out.println(tcl);  // sun.misc.Launcher$AppClassLoader

        // 可以手动设置
        Thread.currentThread().setContextClassLoader(myLoader);
    }
}

本节小结

双亲委派的核心逻辑:

加载请求 → 检查是否已加载 → 委派给父加载器

                    父加载器重复上述流程

                 直到 Bootstrap 或父返回 Class

                 如果父返回 null → 自己加载

它的三个核心价值:安全(防篡改)、唯一性、层次一致性

打破它的场景是 SPI——通过线程上下文类加载器,让 Bootstrap 类能访问 Application 层的类。

下一节,我们来看 类的主动使用 vs 被动使用,理解初始化时机的具体细节。

基于 VitePress 构建