什么是双亲委派模型?

在 Java 虚拟机(JVM)中,双亲委派模型(Parent Delegation Model)是类加载器(ClassLoader)加载类时遵循的核心机制。

其工作流程如下:

当一个类加载器收到类加载请求时,它不会立即尝试自己加载,而是先将请求委派给父类加载器
父类加载器会继续向上委派,直到启动类加载器(Bootstrap ClassLoader)。
只有当所有父类加载器都无法加载该类时,当前类加载器才会尝试自己加载。

这种“自顶向下委派,自底向上加载”的机制,构成了 Java 类加载体系的基石。

为什么需要双亲委派?

双亲委派模型带来了两大核心优势:

  1. 安全性保障
    核心 Java 类库(如 java.lang.Object)由 Bootstrap ClassLoader 加载。若允许应用类加载器自行加载同名类,可能导致核心 API 被恶意替换,破坏 JVM 安全性。

  2. 避免类重复加载
    确保同一个类在 JVM 中只会被加载一次,防止因多个加载器加载同一类而引发 ClassCastExceptionLinkageError


何时需要打破双亲委派?

尽管双亲委派模型非常优秀,但在某些特殊场景下,必须打破它才能实现预期功能。

场景一:JDBC 的 SPI 机制

Java 的 JDBC 是典型的 服务提供者接口(SPI)设计。
DriverManager 由 Bootstrap ClassLoader 加载,但它需要加载用户提供的数据库驱动(如 com.mysql.cj.jdbc.Driver),而这些驱动位于应用的 classpath 中,Bootstrap ClassLoader 无法访问

如果严格遵循双亲委派:

  • DriverManager → 委派给父加载器(即自身)→ 找不到驱动类 → 加载失败。

解决方案
JDBC 使用 线程上下文类加载器Thread.currentThread().getContextClassLoader()),通常是 AppClassLoader,来加载驱动类。
这样就绕过了双亲委派,实现了从“上层”加载“下层”类的能力。

✅ 这是 Java 官方认可的、对双亲委派的合理“打破”。

场景二:Tomcat 的 Web 应用隔离

Tomcat 作为 Web 容器,需要同时运行多个 Web 应用,每个应用可能依赖不同版本的相同类库(如 log4j 1.x 与 2.x)。

若使用标准双亲委派:

  • 所有 Web 应用共享 AppClassLoader → 类版本冲突不可避免。

Tomcat 的解决方案
为每个 Web 应用创建独立的 WebAppClassLoader,并反转类加载顺序

  1. 优先尝试自己加载(从 WEB-INF/classesWEB-INF/lib);
  2. 若找不到,再委派给父加载器(如 CommonClassLoader)。

🔁 这种“先自己,再父类”的策略,主动打破了双亲委派,实现了应用级别的类隔离。


如何自定义打破双亲委派?

虽然可以打破双亲委派,但需谨慎操作。标准做法如下:

❌ 不推荐:重写 loadClass() 方法

java
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    if (name.startsWith("com.myapp.")) {
        return findClass(name); // 跳过父类,直接加载
    }
    return super.loadClass(name);
}

这种方式完全绕过父加载器,容易破坏类加载一致性,可能导致安全问题或类冲突。

✅ 推荐:仅重写 findClass() 方法

保留 loadClass() 的默认双亲委派逻辑,只在父类加载失败后,由子类通过 findClass() 加载特定类:

java
public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 自定义类加载逻辑:读取字节码、defineClass 等
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException(name);
        }
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] loadClassData(String className) {
        // 实现类字节码加载逻辑
        return null;
    }
}

📌 注意:loadClass() 的默认实现已包含完整的双亲委派流程,只需重写 findClass() 即可安全扩展


总结

项目说明
双亲委派作用保证安全性、避免类重复加载
典型打破场景JDBC SPI、Web 容器(如 Tomcat)
正确打破方式使用线程上下文类加载器,或自定义 ClassLoader 并谨慎重写逻辑
最佳实践优先重写 findClass(),而非 loadClass()