你在面试里被问"类加载"时,面试官通常不是想听概念,而是在确认你能不能解释清楚这几件事:
- 一个类到底是被谁加载的?为什么?
- 为什么要"双亲委派"?它解决了什么工程问题?
- 为什么有些场景要打破双亲委派?(SPI / Tomcat)
- 线上出现
ClassNotFoundException/NoSuchMethodError/ 类冲突,怎么定位?
这篇按"从能讲清楚 -> 能落地排查"的主线,把类加载讲透。
1. 先把角色认清:有哪些类加载器
在常见 HotSpot/JDK 体系里(细节随版本略有变化),你可以按三层记:
- BootstrapClassLoader(启动类加载器) :加载
java.lang.*等核心类库 - PlatformClassLoader(平台类加载器):加载平台扩展(JDK 9+ 的模块化体系下替代部分 Ext 概念)
- AppClassLoader(应用类加载器):加载应用 classpath 下的类
通常你的业务类都是 AppClassLoader 加载的。
2. 双亲委派模型:到底"委派"了什么
一句话结论:
- 子加载器收到加载请求时,先把请求交给父加载器尝试;父加载器加载失败,子加载器再尝试加载。
注意:
- "双亲"说的是 父加载器链路,不一定是 Java 继承关系的 parent。
3. 加载流程拆解(按你截图的顺序描述)
你可以把 loadClass() 的逻辑拆成三步(面试最稳的讲法):
3.1 第一步:先查缓存(避免重复加载)
- 先检查这个类是否已经被当前加载器加载过(JVM 内部会维护已加载类的缓存)。
3.2 第二步:向上委派(从下到上)
假设由 AppClassLoader 收到 class 加载请求:
AppClassLoader不直接加载,先委派给PlatformClassLoaderPlatformClassLoader再委派给BootstrapClassLoader
3.3 第三步:父加载器失败后,再向下尝试(从上到下)
- 若
BootstrapClassLoader找不到该类,则回到PlatformClassLoader尝试加载 - 若
PlatformClassLoader也找不到,再回到AppClassLoader尝试加载 - 若
AppClassLoader也找不到,最终抛ClassNotFoundException
你可以用这句话收尾:
- 双亲委派 = 先向上交给父加载器兜底,失败后再逐级回落由子加载器尝试。
4. 为什么需要双亲委派:工程价值比概念更重要
双亲委派解决两个关键问题:
4.1 保护核心类库不被篡改
- 例如
java.lang.String必须由 Bootstrap 加载 - 你在应用里即使写一个同名
java.lang.String,也不会被 AppClassLoader 抢先加载
这就是"安全"的核心:
- 核心类的加载权在最顶层,应用无法覆盖。
4.2 避免类型不一致(同名不同类)
在 JVM 里,"类是否相同"不仅看类名,还看:
- 类全限定名 + 加载它的 ClassLoader
如果同名类被不同加载器各加载一份:
com.xxx.User(A loader)和com.xxx.User(B loader)在 JVM 看来是两个不同类型
后果可能是:
ClassCastException(看起来一样的类,却转型失败)
5. 为什么要打破双亲委派:不是"任性",是为了解耦/隔离
常见的两个代表场景:
5.1 SPI(Service Provider Interface)
典型矛盾:
- 接口在
rt.jar/JDK(由 Bootstrap 加载) - 实现类在应用 jar(由 AppClassLoader 加载)
如果严格双亲委派:
- Bootstrap 想加载实现类,会失败(它看不到应用 classpath)
解决思路:
- 通过线程上下文类加载器(TCCL),让"上层加载器在需要时借用下层加载器去加载实现类"。
5.2 Tomcat(以及多数 Java 容器)的类隔离
容器要解决的问题:
- 一个 JVM 里跑多个应用(多个 war)
- 不同应用可能依赖不同版本的同一个库
做法:
- 让每个应用拥有自己的 ClassLoader(WebAppClassLoader)
- 优先从应用自身加载(一定程度上打破双亲委派)
目的:
- 应用间依赖隔离
6. 常见异常与定位路线(线上排查必须会)
6.1 ClassNotFoundException vs NoClassDefFoundError
ClassNotFoundException:通常是"运行时尝试加载类失败"(例如反射、Class.forName)NoClassDefFoundError:通常是"编译期存在,运行期类加载/链接阶段失败"(依赖缺失、静态初始化失败等)
6.2 NoSuchMethodError / NoSuchFieldError
高概率是:
- 依赖版本冲突(编译时用的版本与运行时实际加载的版本不一致)
6.3 ClassCastException(尤其是"看起来一样的类")
典型信号:
- 多 ClassLoader 场景下,同名类被不同加载器加载
排查关键点:
- 打印对象的类加载器:
java
System.out.println(obj.getClass().getClassLoader());
System.out.println(SomeClass.class.getClassLoader());
7. 怎么快速验证"类到底从哪里来的"
- JVM 启动参数:
-verbose:class(输出会很多,适合本地复现) - 线上观察(视 JVM 版本支持):
jcmd <pid> VM.classloaders
你要形成一个习惯:
- 先确认"实际加载的是哪个 jar/哪个加载器",再谈修复。
8. 面试表达(30 秒讲清楚)
- 双亲委派的核心是:子加载器先委派给父加载器,父加载器找不到再由子加载器加载。
- 工程价值:保护核心类库不被篡改 + 避免同名类被不同加载器重复加载导致类型不一致。
- 打破场景:SPI 通过 TCCL 解决"父加载器看不到子类实现"的矛盾;Tomcat 通过多 ClassLoader 做应用隔离。
- 排查类冲突:看类由哪个 ClassLoader 加载、来自哪个 jar,重点定位版本冲突与多加载器导致的类型不一致。
9. 总结
- 双亲委派不是"规定",是为了安全与一致性
- 打破委派也不是"特例",是为了解耦、可插拔、隔离
- 线上排查永远先问:类从哪来(jar/loader),再做依赖与加载器链路修复