这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 类加载决定类从哪里来、何时初始化、由谁隔离、初始化执行什么。生产中常见的 NoClassDefFoundError、ClassNotFoundException、LinkageError、同名类冲突、热部署失败和 Agent 干扰,都要回到类加载机制分析。
3.1 类生命周期
Loading 加载
-> Verification 验证
-> Preparation 准备
-> Resolution 解析
-> Initialization 初始化
-> Using 使用
-> Unloading 卸载
各阶段职责:
| 阶段 | 职责 |
|---|---|
| Loading | 读取字节流,生成运行时表示 |
| Verification | 校验格式、语义和安全 |
| Preparation | 静态字段分配内存并设零值 |
| Resolution | 常量池符号引用转直接引用 |
| Initialization | 执行 <clinit> |
验证、准备、解析常合称为 Linking。
3.2 类初始化
public class Config {
public static final int VERSION = 1;
public static int counter = initCounter();
static {
System.out.println("Config static block");
}
private static int initCounter() {
return 10;
}
}
准备阶段:
counter = 0
初始化阶段:
counter = initCounter()
执行静态代码块
触发初始化的常见场景:
- new 对象;
- 访问非 final 静态字段;
- 调用静态方法;
- 反射调用;
- 初始化子类时先初始化父类;
- main 所在类。
不触发初始化的示例:
System.out.println(Config.VERSION);
编译期常量会在使用处内联,具体行为以编译结果和规范为准。
3.3 类加载器层次
Java 9 之后:
Bootstrap ClassLoader
-> Platform ClassLoader
-> Application ClassLoader
-> Custom ClassLoader
查看:
System.out.println(String.class.getClassLoader());
System.out.println(java.sql.Connection.class.getClassLoader());
System.out.println(App.class.getClassLoader());
| 加载器 | 加载内容 |
|---|---|
| Bootstrap | 核心类库 |
| Platform | 平台模块 |
| Application | classpath / modulepath 应用类 |
| Custom | 业务隔离、热部署、插件 |
Bootstrap 在 Java 9 后可能显示为 null 或由内部实现表示,不应依赖输出字符串判断。
3.4 双亲委派
流程:
Custom Loader
-> Application
-> Platform
-> Bootstrap
-> 找不到再逐层向下
价值:
- 核心类库不被随意替换;
- 避免重复加载;
- 保证类型一致性;
- 提升安全性。
破坏场景:
- Web 容器隔离应用;
- SPI 需要父加载器访问子实现;
- OSGi 模块化;
- 热部署;
- 测试 Mock;
- Java Agent 增强。
破坏双亲委派不是坏设计,但必须明确隔离边界和生命周期。
3.5 SPI 与线程上下文类加载器
JDBC 示例:
DriverManager.getConnection(
"jdbc:postgresql://db:5432/app",
"app",
"password"
);
服务加载:
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver driver : drivers) {
System.out.println(driver.getClass().getName());
}
问题:核心模块中的 DriverManager 需要加载应用 classpath 的驱动。
解决思路:使用线程上下文类加载器。
查看:
Thread.currentThread().getContextClassLoader()
线程池会复用线程,任务中修改上下文类加载器必须谨慎。
3.6 常见异常
| 异常 | 含义 |
|---|---|
| ClassNotFoundException | 显式加载时找不到类 |
| NoClassDefFoundError | 编译时存在,运行时链接失败 |
| LinkageError | 链接一致性失败 |
| ClassCastException | 类型转换失败 |
| UnsatisfiedLinkError | native 库找不到或不匹配 |
排查:
jar tf app.jar | grep 'com/example/Missing.class'
find ~/.m2/repository -name '*.jar' | xargs grep -l 'Missing.class'
排查方向:
- jar 是否打包;
- classpath 顺序;
- 版本冲突;
- 类加载器隔离;
- 初始化失败后的二次访问;
- 模块系统 exports;
- 动态代理生成失败。
3.7 类冲突诊断
查看类来源:
Class<?> clazz = Class.forName("com.example.Foo");
System.out.println(clazz.getProtectionDomain().getCodeSource().getLocation());
Arthas:
sc -d com.example.Foo
Maven 依赖树:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core
Gradle:
gradle dependencies --configuration runtimeClasspath
常见冲突:
| 库 | 风险 |
|---|---|
| Jackson | 版本不匹配导致序列化失败 |
| Guava | 内部 API 变化 |
| Log4j / Logback | 绑定冲突 |
| Netty | 多版本共存 |
| ASM | 字节码版本不支持新 class |
3.8 自定义类加载器
public class DirectoryClassLoader extends ClassLoader {
private final Path base;
public DirectoryClassLoader(Path base, ClassLoader parent) {
super(parent);
this.base = base;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = Files.readAllBytes(base.resolve(name.replace('.', '/') + ".class"));
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
使用:
ClassLoader loader = new DirectoryClassLoader(
Path.of("/tmp/classes"),
Thread.currentThread().getContextClassLoader()
);
Class<?> clazz = Class.forName("com.example.Plugin", true, loader);
Object plugin = clazz.getConstructor().newInstance();
注意:
- 类名和字节码一致;
- defineClass 只能调用一次;
- 包权限和签名;
- parent 选择;
- 资源加载;
- 卸载条件。
3.9 热加载与卸载
类卸载条件较苛刻:
1. 该 Class 的所有实例已回收
2. 加载它的 ClassLoader 已回收
3. Class 对象没有其他引用
热部署常见问题:
- 旧类实例仍存在;
- 静态状态不重置;
- 类加载器泄漏;
- Metaspace 增长;
- 框架缓存旧类;
- 线程持有旧 ClassLoader。
开发环境热替换与生产全量发布是不同问题,不要混用。
3.10 Java 模块系统
Java 9 引入 JPMS。
module-info.java:
module com.example.order {
requires java.sql;
requires com.fasterxml.jackson.databind;
exports com.example.order.api;
opens com.example.order.model to com.fasterxml.jackson.databind;
}
常见错误:
| 错误 | 原因 |
|---|---|
| package is not visible | 未 exports |
| module does not read package | 未 requires |
| reflection denied | 未 opens |
Spring Boot 应用通常以非模块化路径运行,但依赖库可能带有 module-info,具体由启动方式和模块系统决定。
本章小结
类加载分为加载、链接和初始化,双亲委派保证核心类库一致,SPI 和容器隔离需要打破默认委派。排查类问题要确认类来自哪个 jar、由哪个加载器加载、是否版本冲突、是否初始化失败。自定义加载器和热部署要特别关注 Metaspace 和 ClassLoader 泄漏。
思考题
- 准备阶段和初始化阶段分别做什么?
- 双亲委派解决什么问题?
ClassNotFoundException和NoClassDefFoundError有什么区别?- 如何定位同名类来自哪个 jar?
- 为什么热部署可能造成 Metaspace 增长?