Java 双亲委派模型
目录
-
概览
-
关键概念与角色
-
设计目标与动机
-
loadClass 的典型实现与加载流程
-
findClass 与 defineClass 的协作
-
类身份的运行时判断规则
-
三类破坏双亲委派的典型场景
-
工程实践建议与排查要点
-
常见故障
-
小结
1.概览
双亲委派模型是 Java 类加载器的一种核心加载策略。它规定类加载请求在加载器链中如何向上委托,从而保证同一个限定名的类尽可能由同一个加载器完成定义,避免命名冲突与运行时类型不一致问题。本文以源码要点和实现细节为主线,讲清模型的目标、流程、实现与常见破坏场景,并给出工程实践建议,方便直接用于技术博客发布。
2.关键概念与角色
Bootstrap ClassLoader 引导加载器,由 JVM 实现,负责加载 JRE 核心类库(在 Java 层通常表现为 parent == null)。
Extension ClassLoader 扩展加载器,负责加载 jre/lib/ext 或扩展目录下的类。
Application ClassLoader 应用加载器,负责加载应用 classpath 下的类,通常是线程上下文类加载器的默认值。
自定义 ClassLoader 开发者继承 java.lang.ClassLoader,实现 findClass,从自定义来源读取字节流。
parent 字段 表示委托目标,不是继承关系,而是组合关系。
defineClass 方法 是 final 的,将字节流变成 Class 对象,后续验证、准备、解析、初始化由 JVM 或基础类库统一完成。
3.设计目标与动机
双亲委派模型的核心目标是保证类的唯一性:在被动情况下,遇到某限定名的类,加载请求应优先由更上层的加载器处理,只有父加载器无法找到时,子加载器才尝试加载。这样可以:
优先使用核心类库实现,避免被应用或第三方 jar 覆盖导致安全或一致性问题。
减少命名空间冲突,保证类型判断一致性,避免 ClassCastException 等问题。
4.loadClass 的典型实现与加载流程
java.lang.ClassLoader.loadClass 是双亲委派逻辑的所在。下面是简化的伪代码,便于理解流程:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查缓存
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 委托父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// parent == null 表示 bootstrap 层
c = findBootstrapClassOrNull(name);
}
// 3. 父加载失败,子加载器尝试
if (c == null) {
c = findClass(name); // 由具体加载器实现如何读取字节流
}
}
if (resolve) resolveClass(c);
return c;
}
}
要点说明
findLoadedClass 命中已加载缓存,避免重复定义。
parent == null 表示到达 bootstrap 层,其查找由 JVM 内部实现完成。
findClass 由具体 ClassLoader 实现,例如 URLClassLoader 通过 URLClassPath 查找资源再调用 defineClass。
defineClass 为 final,把字节流变为 Class 的后续步骤由统一机制处理,保证一致性和安全性。
5.findClass 与 defineClass 的协作
以 URLClassLoader 为例,findClass 的工作流程可以概括为:把限定名转换为路径,从资源集合中查找 class 文件,读取字节数组后交给 defineClass。
示例伪码:
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
byte[] bytes = res.getBytes();
return defineClass(name, bytes, 0, bytes.length, protectionDomain);
}
throw new ClassNotFoundException(name);
}
设计意图
各类加载器负责如何获取字节流,不负责把字节流变为运行时类对象的细节。
统一的 defineClass 与 JVM 层处理后续步骤,既简化实现也保证安全与一致性。
6.类身份的运行时判断规则
在 JVM 中,一个类的身份由三要素决定:
-
完全限定名(fully qualified name)
-
加载该类的类加载器
-
保护域(ProtectionDomain)和来源等运行时属性
因此,即便两个类字节码完全相同,只要由不同类加载器加载,它们也被视为不同类型,这就是发生 ClassCastException 的根本原因。
7.三类破坏双亲委派的典型场景
尽管双亲委派是默认且保守的策略,但在工程实践中存在多种会破坏或绕过该策略的情况,下面列出三种典型场景并解释原因与影响。
(一) 主动覆盖 loadClass
开发者可以直接重写 ClassLoader.loadClass 并改变委托顺序。如果子加载器优先加载某些类,就会导致同名类在不同加载器中重复定义,产生命名空间分离。该问题难以调试并会引发类型不一致等错误。
建议:避免重写 loadClass,必要时通过重写 findClass 来实现自定义加载逻辑,保留父委托语义。
(二) SPI ServiceLoader 及 JDK 扩展点的向下查找
JDK 的 SPI 机制(例如 ServiceLoader 或 DriverManager)在加载第三方实现时通常使用线程上下文类加载器(Thread Context ClassLoader)。这意味着核心库代码在运行时会触发应用类加载器去加载实现,从“调用关系”上看是上层触发下层加载,从而在逻辑上破坏了自上而下的严格委派。这是为支持可插拔扩展而做出的折中设计。
(三)热部署、插件与模块化的动态加载
容器或热重载框架会在运行期频繁创建与销毁自定义类加载器,并加载不同版本的类,导致 JVM 内存在多个同名类的定义。若没有清晰的隔离与回收策略,会造成内存泄漏和类型不匹配。
8.工程实践建议与排查要点
避免重写 loadClass,除非非常必要。优先选择重写 findClass,并保持父委派语义。
明确模块边界:在容器或插件架构中设计好哪些包由父加载、哪些包由子加载。
谨慎使用线程上下文类加载器:使用时显式设置并在完成后恢复,避免 ServiceLoader 加载到意外实现。
热部署注意资源释放:释放线程、静态引用、监听器、线程本地变量,确保类加载器可回收。
调试技巧:打印类加载器来源 SomeClass.class.getClassLoader() 或查看 ProtectionDomain 的来源信息以定位问题。
9.常见故障
ClassCastException
检查相关类的 getClassLoader() 是否不同;查找是否存在重复 jar 或自定义类加载器。
NoClassDefFoundError / ClassNotFoundException
检查 classpath、模块边界与线程上下文类加载器设置;确认服务描述文件(META-INF/services)是否正确。
内存泄漏(热部署后不回收)
检查静态字段、线程池、事件监听器、线程本地变量是否持有类加载器或类引用。
SPI 加载到错误实现
检查 Thread Context ClassLoader 的值与 META-INF/services 冲突情况。
10.小结
小结:双亲委派是 JVM 提供的一种保守默认策略,目的是保证核心类库优先加载并尽量避免命名冲突与类型不一致。工程中常见的破坏方式包括覆盖 loadClass、SPI 引发的向下查找,以及容器或热部署带来的动态加载。设计时应明确类加载器边界并管理好生命周期与引用清理。
更多推荐



所有评论(0)