双亲委派模型
双亲委派模型是 Java 类加载机制的核心原则之一。它规定类加载器收到加载请求后,先把请求交给父加载器处理,父加载器无法加载时,当前加载器才尝试加载。
# 1. 工作流程
类加载请求的大致流程:
应用类加载器 -> 平台类加载器 -> 启动类加载器
加载时先向上委派,真正加载时再从上往下尝试。
先问父亲能不能加载
父亲不能加载
自己再加载
# 2. 为什么需要双亲委派
双亲委派有两个核心价值:
# 2.1 避免核心类被篡改
如果用户自己写一个类:
package java.lang;
public class String {
}
双亲委派会优先让启动类加载器加载 JDK 自带的 java.lang.String,避免核心类库被替换。
# 2.2 避免类重复加载
如果父加载器已经加载过某个类,子加载器就不需要重复加载。
这保证了核心类在 JVM 中的唯一性。
# 3. loadClass 的基本逻辑
ClassLoader.loadClass 的典型逻辑:
- 检查类是否已经加载。
- 委派给父加载器。
- 父加载失败后调用
findClass。 - 必要时进行链接。
因此自定义类加载器通常重写 findClass,不要轻易重写 loadClass。
# 4. 双亲委派不是强制规范
双亲委派是推荐模型,不是 JVM 强制所有类加载器必须遵守的规则。
一些框架和容器会有意打破或调整双亲委派。
常见场景:
- JDBC SPI。
- JNDI。
- OSGi。
- Tomcat Web 应用类加载。
- 插件系统。
# 5. Tomcat 为什么要打破双亲委派
Tomcat 需要实现 Web 应用隔离。
不同 Web 应用可能依赖不同版本的同一个类:
app1: commons-lang 2.x
app2: commons-lang 3.x
如果完全遵守双亲委派,公共父加载器加载后,多个应用可能无法隔离依赖。因此容器会设计自己的类加载策略。
# 6. 常见问题
ClassNotFoundException:类加载器找不到类。NoClassDefFoundError:编译时存在,运行时加载失败。ClassCastException: A cannot be cast to A:同名类由不同类加载器加载。LinkageError:类链接阶段出现冲突。
# 7. 双亲委派流程图
应用类加载器收到加载请求
│
▼
是否已经加载过?
│
┌─────┴─────┐
│是 │否
▼ ▼
返回 Class 委派给父加载器
│
▼
父加载器能加载?
│
┌──────┴──────┐
│能 │不能
▼ ▼
返回父加载结果 当前加载器 findClass
这个流程优先保证父加载器的类优先级,因此 Java 核心类库不会被应用类随意替换。
# 8. 打破双亲委派的典型模式
双亲委派不是绝对不能破坏,关键是为什么破坏。
| 场景 | 为什么需要调整 |
|---|---|
| SPI | 父层接口需要加载子层实现 |
| Tomcat | 不同 Web 应用需要依赖隔离 |
| OSGi | 模块之间需要精细化导入导出 |
| 插件系统 | 插件依赖不能污染主应用 |
| 热部署 | 新旧版本类需要隔离 |
破坏双亲委派不是目的,类隔离和扩展能力才是目的。
# 9. 安全边界
双亲委派的安全意义可以用 java.lang.String 举例。
如果没有双亲委派,应用 classpath 中伪造一个:
package java.lang;
public class String {
}
可能会尝试替换核心类。双亲委派让启动类加载器优先加载 JDK 自带 String,避免基础类型体系被破坏。
# 10. 开发中常见问题
# 10.1 为什么依赖冲突会运行时报错
编译和运行加载到的类版本不同,可能导致:
NoSuchMethodErrorNoSuchFieldErrorLinkageError
排查时要看最终运行时 classpath,而不是只看 IDE 中能否编译。
# 10.2 为什么容器里同一个 jar 放错位置会出问题
如果 jar 放在容器公共 lib 下,可能由父加载器加载,所有应用共享;放在 Web 应用内,则由应用类加载器加载。位置不同,类隔离和版本冲突表现完全不同。
# 11. Tips 快问快答
Q:双亲委派中的“父”是父类吗?
A:不是,是父类加载器,不是 Java 继承意义上的父类。
Q:为什么自定义类加载器一般重写 findClass?
A:loadClass 默认包含双亲委派逻辑,重写 findClass 可以复用默认委派流程。
Q:Tomcat 完全不使用双亲委派吗?
A:不是。Tomcat 有自己的类加载策略,在 Web 应用类加载上做了调整,但不是所有加载都完全反过来。
Q:双亲委派能解决所有类冲突吗?
A:不能。它保证核心类优先和部分唯一性,但复杂应用仍可能有依赖版本冲突。
# 12. 总结
双亲委派保证了核心类库安全和类加载稳定性,但在 SPI、容器和插件化场景中会被有意识地调整。理解它不是为了死记规则,而是为了能解释类冲突和框架类加载行为。