# 47. 双亲委派模型如何防止类冲突?哪些场景会破坏双亲委派?

# 标准答案

双亲委派模型(Parent Delegation Model)通过 逐级向上委托类加载,确保 核心 Java API 只能由 Bootstrap ClassLoader 加载,防止自定义类覆盖 JDK 关键类,避免类冲突。该模型的基本逻辑是:一个类加载器在尝试加载类时,先委托给父类加载器,只有当父类找不到时,子类加载器才会尝试加载

然而,在 SPI 机制、类隔离、动态代理 等场景中,双亲委派模型可能会被破坏。

# 答案解析

# 1. 双亲委派模型如何防止类冲突?

  • 核心机制:逐级委托加载

    1. 类加载器收到类加载请求后,优先交给 父加载器 处理。
    2. 一直向上委托,直到 Bootstrap ClassLoader(根加载器)。
    3. 如果父加载器无法找到该类,才会由当前加载器尝试加载。
  • 防止类冲突的关键点

    1. 避免用户代码篡改 Java 核心类
      • 例如 java.lang.Stringjava.lang.Object 只能由 Bootstrap ClassLoader 加载,防止恶意篡改系统类。
    2. 保证不同类加载器的类不会互相干扰
      • 例如 javax.crypto 相关类必须由 JDK 自带的类加载器加载,否则可能被篡改导致安全漏洞。
  • 类加载器层级

    • Bootstrap ClassLoader(启动类加载器):加载 rt.jar,包含 java.lang.*java.util.* 等核心类。
    • ExtClassLoader(扩展类加载器):加载 lib/ext/ 目录下的扩展类,如 javax.crypto
    • AppClassLoader(应用类加载器):加载 classpath 下的应用代码,如 com.example.Main
    • 自定义 ClassLoader:如 Tomcat WebAppClassLoaderSPI 机制 ClassLoader,可以加载用户定义的类。

# 2. 哪些场景会破坏双亲委派模型?

尽管双亲委派模型有效防止类冲突,但在某些场景下,为了满足特定需求,会 人为打破双亲委派机制。常见的破坏场景包括:

  1. Java SPI(Service Provider Interface)机制

    • SPI 需要 动态加载第三方实现,如 JDBC 需要加载 MySQL Driver,但 java.sql.Driver 由 Bootstrap ClassLoader 加载,而 MySQL Driver 由 AppClassLoader 加载,导致父加载器无法找到 MySQL 相关类。
    • 解决方案:Thread.currentThread().getContextClassLoader()当前线程使用子 ClassLoader 加载 SPI 实现
  2. OSGi(模块化类加载)

    • 在 OSGi 体系下,每个模块有自己的 ClassLoader,模块间可以 部分共享类,但为了避免类冲突,OSGi 不遵循双亲委派,而是采用 自定义的类加载策略(如 Import-PackageExport-Package)。
    • 例如,Tomcat 使用 不同 WebApp 各自独立的 WebAppClassLoader,确保不同应用的 Servlet 不会互相干扰。
  3. Tomcat、Spring 自定义类加载

    • Tomcat 为了支持 不同 Web 应用相互隔离,每个 WebApp 都有独立的 WebAppClassLoader,避免 WebA 加载的 JAR 干扰 WebB
    • Spring 通过 ClassLoader 动态加载 Bean,如 AOP 代理、字节码增强,可能会破坏默认的委派机制。
  4. 动态代理(JDK Proxy / CGLIB)

    • JDK 动态代理使用 Proxy.newProxyInstance(ClassLoader loader, ...) 创建代理类,其中 loader 可能是 自定义的 ClassLoader,可能与双亲委派机制不兼容。
    • CGLIB 生成子类代理,类加载器可能不同于目标类的 ClassLoader,导致 ClassCastException
  5. 自定义 ClassLoader(热更新、插件机制)

    • 为了支持 JVM 热部署,如 Tomcatreloadable 机制,会自定义 ClassLoader 重新加载类,而不是走双亲委派模型。
    • IDEAEclipse 也会使用 自定义 ClassLoader 实现 热替换(HotSwap),避免 JVM 重新启动。

# 3. 代码示例

破坏双亲委派:自定义 ClassLoader

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 这里可以自定义类加载逻辑,例如从网络或加密文件加载类
        return super.findClass(name);
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        MyClassLoader myLoader = new MyClassLoader();
        Class<?> clazz = myLoader.loadClass("com.example.MyClass");
        System.out.println("Class Loaded: " + clazz.getName());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

破坏 SPI 机制示例

ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
1

JVM 默认使用 AppClassLoader 加载 MyService,如果 MyService 由自定义 ClassLoader 加载,则需要手动指定:

ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class, Thread.currentThread().getContextClassLoader());
1

# 最佳实践

  1. 尽量遵循双亲委派模型,避免类冲突。
  2. 使用 Thread Context ClassLoader 解决 SPI 加载问题,如 JDBC Driver 加载。
  3. 合理设计类加载器层级,如 插件机制 可采用独立 ClassLoader,避免影响主应用。
  4. 避免 ClassLoader 泄露,如 ThreadLocal 持有 ClassLoader 可能导致内存泄漏。

# 深入追问

  1. OSGi 如何实现类隔离?如何解决多个 ClassLoader 互相调用的问题?
  2. Spring 如何使用 ClassLoader 进行 Bean 代理?
  3. Tomcat 如何实现 WebApp 的类加载隔离?

# 相关面试题

  • 什么是 Java SPI?为什么 SPI 机制破坏了双亲委派?
  • Tomcat 如何通过 WebAppClassLoader 实现类隔离?
  • 为什么 ClassLoader 可能会导致内存泄漏?如何避免?