类加载器(ClassLoader)与双亲委派:为什么同一个类能“加载两份”?
·
遇到过这两种离谱问题,你就知道类加载不是“背概念”,而是实打实的工程问题:
- 线上报错:
ClassCastException: A cannot be cast to A - 本地启动:
NoSuchMethodError/NoClassDefFoundError,明明依赖里有
这类问题的根子,往往都在 ClassLoader 隔离 与 双亲委派。
1. 类加载到底加载了什么
JVM 运行时需要两类东西:
- 字节码:
.class的内容 - 类元数据:这个类有哪些字段/方法/父类/接口、常量池等
“类加载”不是简单读文件,它的产物是 JVM 里的一个 Class 对象,并且有一个非常关键的身份判定:
- 类的唯一性 = 类全限定名 + 加载它的 ClassLoader
所以:
- 同一个
com.xxx.User,被两个不同的ClassLoader加载出来,就是 两个互不兼容的类型。
2. 类加载全过程:加载 -> 验证 -> 准备 -> 解析 -> 初始化
你常用的理解顺序:
- 加载(Loading):找到字节码来源(文件/网络/内存/加密包),读进来
- 验证(Verification):字节码合法性校验
- 准备(Preparation):给
static变量分配内存并设默认值 - 解析(Resolution):把常量池里的符号引用变成直接引用
- 初始化(Initialization):执行
<clinit>(静态变量赋值、静态代码块)
排障上最重要的是初始化:
- 什么时候触发初始化?
new- 访问静态字段/方法
- 反射调用
- 子类初始化前会先初始化父类
3. 常见 ClassLoader 分层(你可以按“谁加载谁”记)
典型分层(不同 JVM 实现略有差异,但思路一致):
- 启动类加载器:加载核心类库(如
java.lang.*) - 平台/扩展类加载器:加载平台级库
- 应用类加载器:加载应用 classpath 下的类(我们大多数业务类在这)
- 自定义类加载器:框架/容器/插件系统常见(Tomcat、OSGi、SPI、热加载等)
4. 双亲委派:它到底“委派”了什么
双亲委派的核心规则:
- 一个类加载请求到来时
- 先交给父加载器尝试加载
- 父加载器加载不了,子加载器才自己加载
目的:
- 避免核心类库被篡改(你写一个
java.lang.String不能覆盖系统的) - 减少重复加载(父加载过,子就复用)
注意:
- “双亲”不是指 Java 的继承关系,而是 ClassLoader 的逻辑父子关系
5. 为什么会出现 A cannot be cast to A
你看到的 A 是同名类,但来自不同的 ClassLoader:
A@ClassLoader1与A@ClassLoader2是两种类型- JVM 认为它们不是一个类
常见触发场景:
- 容器/插件:Tomcat 的 WebAppClassLoader
- 依赖冲突:同一 jar 被打进了两份(不同路径/不同版本)
- 自定义加载器:热部署、脚本引擎
排查思路:
- 打印
obj.getClass().getClassLoader() - 对比两边 ClassLoader 是否一致
6. JDK 自带的一个“低成本确认”:看类是从哪加载的
你可以这样确认某个类来自哪个 jar:
User.class.getProtectionDomain().getCodeSource().getLocation()
以及确认加载器:
User.class.getClassLoader()
7. 你在面试/实战都绕不开的:SPI 与线程上下文类加载器(TCCL)
很多人知道双亲委派,但会被一个问题卡住:
- JDK 的核心类(父加载器)怎么“反过来”加载你业务 jar 里的实现?
典型例子:JDBC。
解决思路:
- 通过 线程上下文类加载器(TCCL)
- 让框架在需要加载实现类时,用
Thread.currentThread().getContextClassLoader()去加载
这也是为什么你在容器里看到各种“打破双亲委派”的实现:
- 不是为了破坏规则,而是为了实现 隔离 + 可插拔。
8. 最后总结:你应该怎么把它讲清楚
- 类唯一性:
全限定名 + ClassLoader - 双亲委派:先父后子,防篡改、少重复
- 容器/插件系统:靠不同 ClassLoader 做隔离
A cannot be cast to A:大概率就是两个 ClassLoader
更多推荐




所有评论(0)