文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Java JVM 双亲委派(Parent Delegation)机制

本文旨在把 JVM 双亲委派(Parent Delegation)机制 做一个系统、源码级别且实用的详解:为什么要有它、在 Java 层大致实现是什么样子、热点实现细节(包括并行加载的演进)、常见变体(child-first/容器例外)与常见陷阱(类身份、ClassCastException、类泄漏、TCCL 问题),并配上 mermaid 流程图与关键伪码方便理解。

要点先看一眼

  • 定义:双亲委派是 Java 的类加载策略:一个类加载器在尝试加载类时,先把请求委派给父加载器,父加载器找不到时再由自己去加载。
  • 目的:保证 JVM 核心类(java.* 等)由启动类加载器加载,避免用户类覆盖核心类,保证类型安全与一致性(single source for core libs)。
  • 决定类身份:一个类的“身份”由(类的完全限定名 + 它的定义类加载器)共同决定。两个不同加载器加载同名类是不同类型。
  • 可以被覆盖:应用服务器 / 插件框架常常打破或调整双亲委派(child-first 或 selective delegation)来实现隔离或热部署。

经典行为(伪码,现代 JDK 风格)

下面是简化后的 ClassLoader loadClass 流程伪码(符合现代 JDK 的设计思想:先查已加载 → 父委派 → 自己加载 → 可选解析)。注意同步细节(并行能力),下面展示 getClassLoadingLock 机制的伪实现:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 1. 同步(现代 JVM 用 per-classname lock 支持并行加载)
    synchronized (getClassLoadingLock(name)) {
        // 2. 已加载?(避免重复加载)
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 3. 委派给 parent(parent == null 表示 Bootstrap loader)
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException ignored) {
                // 父找不到 -> 由当前 loader 自己尝试
            }

            // 4. 父也没有 -> 自己 findClass(通常由用户覆盖)
            if (c == null) {
                c = findClass(name); // read bytes -> defineClass(...)
            }
        }

        // 5. resolve 可选:把符号引用解析为直接引用
        if (resolve) resolveClass(c);

        return c;
    }
}

要点说明:

  • findLoadedClass(name):检查此加载器是否已加载过该类(不会检查 parent)。
  • parent.loadClass(...):父委派,可传播到 Bootstrap。Bootstrap loader 在 Java 层通常表示为 null;由 VM 内部实现 findBootstrapClassOrNull
  • findClass(name):由自定义 ClassLoader 覆盖,负责定位/读字节码并调用 defineClass
  • resolveClass:把常量池中的符号引用解析为直接引用(可延迟,也可以在此处执行)。
  • 同步:防止并发重复加载;现代 JDK 通过 getClassLoadingLock(name) 支持并行 capable loaders(ClassLoader.registerAsParallelCapable()),而不是简单地把 synchronized(this) 用作锁,从而提升并发性能。

为什么要使用双亲委派?(动机/历史)

  1. 防止核心类被篡改:例如,java.lang.String 必须只由 Bootstrap loader 提供;双亲委派确保用户定义的同名类不会先被加载。
  2. 类型一致性:JVM 希望某个核心类在整个运行时只有一个定义源,避免类实现冲突导致不可预期错误。
  3. 安全:安全管理器/沙箱依赖于核心库由受信任加载器加载。
  4. 简单性:分层加载器体系使模块化与隔离更容易实现(应用类与平台类明显分离)。

关键细节(经常被误解或导致 bug 的点)

1. Bootstrap loader = null(在 Java 层)

父为 null 表示引导类加载器(由 JVM 实现),它加载平台/核心类(以前是 rt.jar,现在模块系统)。当 parent 为 null 时 ClassLoader 会调用 VM 内部方法来尝试从 bootstrap 类路径加载类。

2. 类的“身份”与 ClassCastException

A 类由系统类加载器加载而 B(同名类)由自定义加载器加载,则 A.classB.class 不是同一个类型。常见后果:向方法传递载入自另一个加载器的实例将抛 ClassCastException
示例:com.example.Plugin$Data 被 host 加载与 plugin 加载两次 -> 不能相互强制转换。

3. findLoadedClass 只查当前加载器的缓存

findLoadedClass 只查“该加载器”是否已经加载过指定类,不会自动检查 parents。loadClass 的父委派先行,因此父加载器加载的类会被返回。

4. 并发加载与 getClassLoadingLock

老版本 JVM 使用同步(this) 导致大量锁竞争;现代 JDK 提供“并行可加载 ClassLoader”机制(ClassLoader.registerAsParallelCapable()),并通过 getClassLoadingLock(name) 为不同类名提供更细粒度锁,从而允许不同线程并行加载不同类。

5. 资源加载也遵循委派(通常)

getResource / getResources 一类的方法也遵循父委派(父先查),但实现细节可以被覆盖且不同容器可能有特殊策略。


常见变体:什么时候不按双亲委派

  1. child-first(子优先)/ parent-last:容器或插件框架为保证插件可以使用自己版本的库,先尝试由子加载器加载,再委派父加载器。

    • 优点:实现版本隔离、热部署更灵活。
    • 缺点:可能会替换核心库,或导致类型冲突(多个同名类存在于不同加载器),需要非常小心。
    • 实现方式:覆写 loadClass,先调用 findLoadedClass,再尝试 findClass,最后委派父。示例如下(简化):
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // child-first: 先自己找(但保留 java.* 的父优先策略)
                if (!name.startsWith("java.")) {
                    c = findClass(name);
                }
            } catch (ClassNotFoundException ignored) { }
            if (c == null) {
                c = super.loadClass(name, false); // 父委派
            }
        }
        if (resolve) resolveClass(c);
        return c;
    }
}
  1. selective delegation(选择性委派):很多应用服务器(Tomcat/Jetty)对某些包做父优先或子优先的例外处理(例如确保 javax.servlet.* 的 API 由容器提供,但应用的实现类由应用加载器提供)。

与 Thread Context ClassLoader(TCCL)的关系

  • TCCL(线程上下文类加载器) 是 JVM 提供的一个 hook:某些库(SPI、JAXB、JNDI、服务加载器 ServiceLoader)会使用 Thread.currentThread().getContextClassLoader() 来加载用户类或服务实现,这样运行在系统/平台代码中的库可以“回退”到应用类加载器加载应用资源。
  • 典型场景:服务发现时(ServiceLoader.load(...))默认使用 TCCL;所以框架在启动线程前常把 TCCL 设置为 webapp 的类加载器以便框架加载 webapp 的类。
  • 风险:如果不正确恢复 TCCL 或在池化线程中不谨慎设置,会导致类/资源被错误加载甚至类加载器泄漏。

类卸载与双亲委派的影响

  • 类的卸载通常与定义该类的 ClassLoader 的回收挂钩(只有当加载器可被 GC 回收,且没有遗留静态引用、线程或 JNI 全局引用时,才能卸载)。
  • 双亲委派关系能影响 GC 可达性:例如父加载器持有对某些类的引用会阻止子加载器所加载类的卸载;复杂容器环境下容易发生“类加载器泄露”。

调试与排错建议

  • ClassCastException 出现时:打印 obj.getClass().getClassLoader()TargetType.class.getClassLoader(),看是否不同。
  • 类重复加载:检查应用是否在多个 classpath / jar 中包含同名类,或是否存在多个 classloader。
  • 类泄露:使用 heap dump / MAT 检查是否有静态集合、线程、ThreadLocal、JNI 全局引用引用着 ClassLoader 或 Class 对象。
  • 并发类加载问题:查找是否自定义加载器未正确实现并发策略(未使用 getClassLoadingLock 等)。

mermaid 流程图(父委派主路径)

在这里插入图片描述

总结(快速回顾)

  • 双亲委派是 JVM 的默认类加载策略,保证了核心类的一致性与安全。
  • Java 层 ClassLoader.loadClass 的典型流程是:已加载检查 → 父委派 → 自己查找(findClass/defineClass)→ 可选解析。
  • 现代 JDK 支持并行类加载锁(getClassLoadingLock),以减少锁竞争。
  • 可以打破双亲委派(child-first / selective),但要非常小心类身份与安全问题。
  • TCCL、ServiceLoader、模块系统、类卸载、类泄漏等与类加载器语义紧密相关,实际工程中需要谨慎设计加载器之间的边界。
posted @ 2025-09-01 10:40  NeoLshu  阅读(10)  评论(0)    收藏  举报  来源