双亲委派机制(原理/优势/沙箱安全)
双亲委派: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() 的标准实现完美体现了这个流程:
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/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 对象。
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.sql.DriverManager 由 Bootstrap 加载(因为它在 rt.jar 中)
// 但 MySQL 的 com.mysql.cj.jdbc.Driver 由 Application ClassLoader 加载
// 如果严格按照双亲委派,Bootstrap 看不到 Application 的类
// DriverManager 就找不到 MySQL 的驱动解决方案:线程上下文类加载器
// 在 DriverManager 加载驱动时:
// Thread.currentThread().getContextClassLoader() 获取线程的上下文加载器
// 用这个加载器去加载 Driver 实现类
// SPI 的标准实现:
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class,
Thread.currentThread().getContextClassLoader());线程上下文类加载器的默认值
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 被动使用,理解初始化时机的具体细节。
