JVM 类加载机制与双亲委派
更新时间:2026-09-18。这是 FAQ「什么是双亲委派模型」的深文版,把类的完整生命周期和为什么/什么时候打破委派讲透。短答见 什么是双亲委派模型。
一、类的生命周期:七步

- 加载(Loading):通过全限定名获取字节流,在方法区生成类元数据,堆上产生对应
Class对象。 - 验证(Verification):校验字节码格式、元数据、符号引用,防止恶意/非法类。
- 准备(Preparation):为静态变量在方法区分配内存并赋零值(如
static int a=1此处先为 0)。 - 解析(Resolution):常量池符号引用替换为直接引用。
- 初始化(Initialization):执行
`<clinit>`,静态变量才真正赋成代码里的值。
二、三类加载器与双亲委派

委派规则:收到加载请求先委托 parent,parent 找不到才自己 findClass。价值:① 核心类(java.lang.String)只由 Bootstrap 加载,防伪造;② 同一类不被重复加载(类身份 = ClassLoader + 全限定名)。
三、何时打破双亲委派
| 场景 | 为什么需要打破 | 做法 |
|---|---|---|
| SPI/JDBC | 核心类(Bootstrap)要加载 classpath 里的厂商驱动 | 线程上下文类加载器 Thread.currentThread().getContextClassLoader() 反向加载 |
| Tomcat | 不同 WebApp 同名依赖版本隔离、可各自热加载 | 每个 WebApp 独立 ClassLoader,优先加载自己 WEB-INF 下的类(核心 java.* 仍委派) |
| OSGi/热部署 | 模块网状依赖、类卸载替换 | 网状委派、自定义加载顺序 |
四、自定义 ClassLoader 与排查
继承 ClassLoader 重写 findClass()(推荐,保留双亲委派):读取 class 字节后调 defineClass;只有想彻底改变加载顺序才重写 loadClass。
java
class MyLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadFromCustomSource(name); // 从加密包/网络/插件目录读
return defineClass(name, b, 0, b.length);
}
}ClassNotFoundException:加载阶段按名找不到类,看 classpath/依赖冲突。NoClassDefFoundError:编译期存在、运行期缺失或初始化失败,或被不同加载器加载成「两个类」导致ClassCastException——排查先确认是哪个 ClassLoader 加载的(obj.getClass().getClassLoader())。
五、常见坑
- 同名类被两个加载器加载:在 JVM 是两个类型,单例失效、强转报错(插件/Tomcat 高发)。
- 自定义加载器泄漏 Metaspace:动态生成类又持有加载器不释放,会导致 Metaspace 涨、Full GC,见 Full GC 频繁怎么排查。
- 重写 loadClass 破坏核心类安全:一般只重写
findClass,否则可能绕过委派加载到伪造核心类。 - 父加载器看不到子加载器的类:SPI 场景必须用上下文加载器,直接
Class.forName在 Bootstrap 下找不到驱动。
总结
JVM 类要经历加载(找字节流、生成 Class 对象)、连接(验证+准备零值+解析)、初始化(执行 <clinit> 真正赋值)才可用;类加载器分 Bootstrap/Platform/Application 三级并默认双亲委派——先委托父加载器、父找不到才自己加载,以此保证核心类不被伪造、类不重复加载;但 SPI 用线程上下文加载器、Tomcat 用每个 WebApp 独立且优先本地类的加载器、OSGi 用网状委派来满足反向可见与依赖隔离,自定义时只重写 findClass,排查类冲突要从“哪个 ClassLoader 加载了类”入手,并警惕动态类加载导致的 Metaspace 泄漏。