跳到主要内容
约 7 分钟3 张卡

双亲委派模型

双亲委派模型是 Java 类加载器默认采用的查找规则:先复用已加载的类,再交给父加载器查找,父加载器找不到时才由当前加载器查找。它让应用和插件能够共享平台类与公共接口;理解这条规则,也有助于分析同名类冲突、依赖隔离和热部署。

1. 面试核心​

先记住这几条,再读正文
  • 默认查找顺序。 loadClass 先通过 findLoadedClass 检查已有记录,再委派父加载器,父加载器找不到时才调用当前加载器的 findClass。卡 · 委派流程
  • 委派的作用。 父优先让多个子加载器复用同一份平台类和公共 API。共享接口由共同的父加载器定义,宿主与插件才能围绕同一种接口类型交互。
  • 运行时类型身份。 JVM 用二进制名称和定义加载器共同标识类。名称和字节码相同、定义加载器不同的类,仍然是不同类型。卡 · 委派的作用
  • 发起加载与定义类型。 发起加载器可以通过委派取得类,定义加载器负责创建该运行时类型。findLoadedClass 查询发起加载记录,Class.getClassLoader() 返回定义加载器。卡 · 发起加载器与定义加载器
  • 内置加载器与父子关系。 JDK 17、21 使用启动、平台和应用加载器。加载器的父子关系通过对象引用建立,描述的是委派关系。
  • 调整委派的边界。 插件可以优先加载私有依赖,但平台类和宿主 API 仍需共享。线程上下文加载器则用于让平台代码发现应用提供者,解决另一类查找需求。

2. 委派流程​

加载一个类之前,加载器要先确认已有记录,避免重复加载。ClassLoader.loadClass 按以下查找顺序处理请求:

  1. findLoadedClass 查询当前加载器的发起加载记录。已有记录时,直接复用对应的类。
  2. 尚未加载时,当前加载器委派给父加载器。父加载器为 null 时,尝试启动类加载器。
  3. 父加载器抛出 ClassNotFoundException 后,当前加载器调用自己的 findClass。
  4. 找到类后,如果调用参数 resolve 为 true,再调用 resolveClass 链接该类。

这里要区分三个方法的职责:loadClass 组织查找流程,findClass 负责当前加载器自己的查找,defineClass 把找到的字节转换为运行时类型。

自定义加载器若只需要改变字节码来源,可以重写 findClass。例如,插件加载器从插件目录读取字节,再调用 defineClass 创建类。这样可以保留默认的委派流程和并发加载同步逻辑。

覆盖 loadClass 会改变整个搜索流程。这时需要明确哪些包父优先、哪些包子优先,以及怎样协调并发加载。默认流程只在父加载器报告查找失败时转入本地查找。如果父加载阶段发生链接错误,就应传播错误,避免把版本不兼容误当成类不存在。

3. 委派的作用​

父优先让多个子加载器复用同一个平台类或公共 API。比如,应用请求 java.lang.String 时,默认委派会先取得启动类加载器定义的类型。

插件之间也需要这种共享。父加载器定义 api.Service 接口,子加载器分别定义插件实现。宿主和插件看到的是同一个接口类型,所以宿主能把插件实例转换为该接口,再调用它的方法。

隔离的子加载器仍可各自定义同名业务类,因为 JVM 用二进制名称和定义加载器共同确定类型。二进制名称是类在运行时使用的名称,例如 com.example.User。两个加载器分别定义这个类时,即使读到的字节相同,也会得到两个不同类型。

因此,插件的私有实现可以分别加载,共享 API 则应由宿主和插件共同可见的加载器定义。仅仅把同一份接口 JAR 分别放进两个插件,并不能保证它们共享同一种类型。

3.1 发起加载器与定义加载器​

发起加载器和定义加载器描述的是两种职责。JVM 通过某个加载器发起加载请求时,这个加载器可以自行定义类,也可以委派其他加载器取得类。实际把字节表示创建为运行时类型的加载器,才是定义加载器。

假设 JVM 通过子加载器请求 api.Service,最后由父加载器定义该类。JVM 会把子加载器记录为这个类的发起加载器,父加载器则是定义加载器。调用 Service.class.getClassLoader(),得到的是父加载器。

findLoadedClass 查询 JVM 记录的发起加载关系,所以也能返回经委派取得的类。判断类型身份时则要看定义加载器,不能只看请求从哪个加载器进入。

4. 内置类加载器​

JDK 17、21 的内置加载器分为三类。它们分担平台类与应用类的加载工作:

加载器职责
Bootstrap加载核心平台类,Java API 中以 null 表示
Platform加载其他平台模块,JDK 9 起替代扩展加载器
Application/System加载应用类路径、模块路径等内容

默认应用加载器以平台加载器为父加载器。JDK 9 引入模块系统后,加载类还要考虑类属于哪个模块,以及模块之间是否允许访问。因此,内置加载器在模块场景下也会根据模块归属跨层委派。

应用服务器、构建工具和插件系统可以增加自定义加载器,形成更复杂的加载器关系。

双亲委派是 ClassLoader 的默认协议。JVM 规范允许自定义加载策略。

5. 父加载器与类继承​

加载器的“父子关系”指请求交给谁处理。getParent() 返回父加载器对象,子加载器通过这个对象引用调用它。也就是说,这种关系通过组合建立,与加载器实现类的 Java 继承关系是两回事。

“双亲”是传统名称,一条默认委派链只有一个直接父节点。

6. 改变委派顺序的场景​

不同应用或插件需要使用不同版本的依赖时,可以采用 child-first:先查自己的类,再按规则委派父加载器。另一些场景会改变发起查找的加载器,或者用新的加载器承载新版类。常见需求包括:

  • 应用服务器让不同应用使用各自版本的业务依赖。
  • 插件系统让插件优先使用私有的第三方库。
  • SPI 由平台代码发起,通过线程上下文加载器发现应用提供者。
  • 热部署用新加载器定义新版业务类,并为旧加载器卸载创造条件。

改变顺序时,平台类和宿主接口仍需共享同一份定义。日志门面、用于传递数据的协议类型也需要按这个边界划分。若两边分别定义这些类,就会出现类型转换失败,或各自维护一份静态状态。旧加载器被线程或静态引用保留时,还会造成资源泄漏。

SPI 的发现流程见 类加载器。该文也介绍热部署时的隔离与卸载条件。

7. 加载结果观察​

排查同名类冲突时,需要同时确认类从哪里读取、最终由谁定义。以下命令分别提供类加载日志、加载器关系与统计信息:

java -Xlog:class+load=info -jar app.jar
jcmd <pid> VM.classloaders
jcmd <pid> VM.classloader_stats

JDK 9 起可用统一日志观察类来源。定位类型冲突时,也可比较定义加载器:

System.out.println(value.getClass().getClassLoader());
System.out.println(Service.class.getClassLoader());

JAR 路径说明字节来源,定义加载器说明类型身份。一个加载器可以从多个 JAR 查找类,同一个 JAR 也可被多个加载器分别读取。所以,仅凭文件路径相同,还无法判断得到的是否为同一类型。

8. 常见问题​

8.1 自己定义 java.lang.String,能替换 JDK 的 String 吗?​

普通应用加载器无法这样替换。默认委派会先取得启动类加载器定义的 String,当前加载器直接复用它。即使改变委派顺序,JDK 21 的 defineClass 也对 java.* 包有保护:只有平台加载器及其祖先可以定义这些类,其他加载器会被拒绝。

8.2 双亲委派能解决依赖版本冲突吗?​

8.3 插件怎样隔离依赖,又共享宿主接口?​

9 · 面试题卡片​

9.1 双亲委派的加载流程和作用是什么​

出现公司:阿里巴巴、字节跳动

考察重点:解释已加载检查、父委派与本地查找的顺序,以及它怎样让多个加载器共享平台类和公共 API。

相关内容:委派流程。

参考回答
  • 流程是先检查已加载记录,再委派父加载器,最后由当前加载器查找。 默认实现依次使用 findLoadedClass、父加载器的 loadClass 和当前加载器的 findClass;父加载器查找失败后才进入最后一步。
  • 作用是优先共享上层定义的平台类和公共接口。 多个子加载器复用同一种类型,宿主与插件才能通过公共 API 交互。

插件或容器可以调整私有依赖的查找顺序,但平台包和宿主 API 仍应共享。隔离的子加载器也仍可分别定义同名业务类。

9.2 为什么同名同字节码的两个类仍可能无法强制转换​

出现公司:字节跳动

考察重点:说明二进制名称和定义加载器怎样共同确定类型,并解释插件接口为什么需要共享。

相关内容:委派的作用。

参考回答

因为类的运行时身份同时包含二进制名称和定义加载器。

两个加载器分别定义 com.example.User,即使字节码完全相同,也会得到两个不同类型,两份类的实例无法互相强制转换。

插件场景应让公共接口由共同的父加载器定义,只隔离实现和私有依赖。这样宿主才能把插件实例转换为共享接口并调用它。

9.3 findLoadedClass 返回的类一定由当前加载器定义吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:区分发起加载记录与类型定义,说明为什么参与加载的加载器与定义加载器可以不同。

相关内容:发起加载器与定义加载器。

参考回答

不一定,它查询的是 JVM 记录的发起加载关系。

当前加载器可以通过委派取得一个由父加载器定义的类,findLoadedClass 也能返回这个类。

Class.getClassLoader() 返回的才是定义加载器。判断运行时类型身份,应把定义加载器和类的二进制名称一起比较。

10. 参考资料​