Java 类加载机制详解
类加载是 JVM 将.class 文件加载到内存并验证、解析、初始化的过程,理解类加载机制对框架开发和故障排查至关重要。
一、类加载过程
flowchart LR
A[加载] --> B[验证]
B --> C[准备]
C --> D[解析]
D --> E[初始化]
1.1 加载(Loading)
三件事:
- 通过全限定类名获取二进制字节流
- 将静态存储结构转化为方法区的运行时数据结构
- 在堆中生成 Class 对象作为访问入口
来源:
- 本地文件系统(.class 文件)
- JAR/WAR 包
- 网络(Applet)
- 动态生成(CGLIB、ASM)
1.2 验证(Verification)
确保字节流符合 JVM 规范:
验证阶段
├── 文件格式验证(魔数、版本号)
├── 元数据验证(语义检查)
├── 字节码验证(数据流、控制流)
└── 符号引用验证(解析前检查)
1.3 准备(Preparation)
为类变量分配内存并设置默认值:
public static int value = 123;
// 准备阶段:value = 0(int 默认值)
// 初始化阶段:value = 123
1.4 解析(Resolution)
将常量池中的符号引用替换为直接引用:
符号引用 → 直接引用
↓
Lcom/example/MyClass; → 0x12345678(内存地址)
1.5 初始化(Initialization)
执行类构造器<clinit>()方法:
static {
// 静态代码块
value = 123;
}
二、类加载器
2.1 加载器层次
graph TB
A[Bootstrap ClassLoader]
B[Extension ClassLoader]
C[Application ClassLoader]
D[Custom ClassLoader]
A -->|parent| B
B -->|parent| C
C -->|parent| D
2.2 启动类加载器(Bootstrap)
实现:C/C++(rt.jar 中)
职责:加载核心类库(java.lang.*, java.util.*等)
路径:$JAVA_HOME/lib
2.3 扩展类加载器(Extension)
实现:sun.misc.Launcher$ExtClassLoader
职责:加载扩展类库
路径:$JAVA_HOME/lib/ext
2.4 应用程序类加载器(Application)
实现:sun.misc.Launcher$AppClassLoader
职责:加载 classpath 中的类
路径:-classpath 或 java.class.path
2.5 查看加载器
// 获取当前类的加载器
ClassLoader loader = MyClass.class.getClassLoader();
System.out.println(loader);
// AppClassLoader
// 获取系统类加载器
ClassLoader system = ClassLoader.getSystemClassLoader();
System.out.println(system);
// AppClassLoader
// 获取平台类加载器(JDK 9+)
ClassLoader platform = ClassLoader.getPlatformClassLoader();
三、双亲委派模型
3.1 工作原理
收到类加载请求
↓
先委托给父加载器
↓
父加载器无法加载
↓
自己尝试加载
3.2 源码分析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委托父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载
}
if (c == null) {
// 3. 自己加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
3.3 优点
-
安全性:防止核心 API 被篡改
// 自定义 java.lang.String 不会被加载 // Bootstrap 加载器优先加载 rt.jar 中的 String -
避免重复加载:父加载器已加载,子类无需再加载
-
保证类的唯一性:同一个类在 JVM 中只有一个 Class 对象
四、破坏双亲委派
4.1 SPI 机制
// JDBC Driver 加载
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 父加载器无法加载厂商实现的 Driver
// 需要线程上下文类加载器(ThreadContextClassLoader)
4.2 热部署
应用服务器(Tomcat)
├── 每个 Web 应用独立 ClassLoader
├── 优先加载应用类
└── 实现热部署、热替换
4.3 代码热替换
// OSGi 模块化框架
// 每个 Bundle 独立 ClassLoader
// 支持模块动态安装、卸载、更新
4.4 自定义加载器
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
private byte[] loadClassData(String className) throws IOException {
String path = classPath + "/" + className.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(path)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
}
五、类加载器实战
5.1 判断类是否相同
// 两个类相同的条件:
// 1. 类名相同
// 2. 类加载器相同
ClassLoader loader1 = new MyClassLoader("/path1");
ClassLoader loader2 = new MyClassLoader("/path2");
Class<?> c1 = loader1.loadClass("com.example.MyClass");
Class<?> c2 = loader2.loadClass("com.example.MyClass");
System.out.println(c1 == c2); // false(加载器不同)
5.2 内存泄漏
// OSGi 场景:Bundle 卸载后 ClassLoader 无法回收
// 原因:静态变量持有 Class 引用
// 解决:清理静态引用,确保 ClassLoader 可回收
5.3 Tomcat 类加载器
Tomcat 类加载层次
├── Bootstrap(JVM 启动)
├── System(系统类库)
├── Common(Tomcat 公共类)
├── Catalina(容器类)
└── WebApp(每个应用独立)
├── WebAppClassLoader
└── 优先加载应用类(反双亲委派)
六、常见问题
6.1 ClassCastException
// 不同 ClassLoader 加载的同一类转换时报错
Object obj = loader1.loadClass("MyClass").newInstance();
MyClass mc = (MyClass) obj; // ClassCastException
// 解决:确保使用同一 ClassLoader
6.2 ClassNotFoundException
// 类不存在或加载器无法找到
try {
Class.forName("com.example.MyClass");
} catch (ClassNotFoundException e) {
// 检查 classpath 或加载器配置
}
6.3 NoClassDefFoundError
// 编译时有,运行时找不到
// 原因:类初始化失败或依赖缺失
七、总结
类加载核心要点:
| 阶段 | 作用 | 关键操作 |
|---|---|---|
| 加载 | 获取字节流 | 文件系统/JAR/网络 |
| 验证 | 安全性检查 | 格式/元数据/字节码 |
| 准备 | 分配内存 | 设置默认值 |
| 解析 | 符号→直接 | 常量池替换 |
| 初始化 | 执行静态代码 | <clinit>() |
| 加载器 | 职责 | 路径 |
|---|---|---|
| Bootstrap | 核心类库 | $JAVA_HOME/lib |
| Extension | 扩展类库 | $JAVA_HOME/lib/ext |
| Application | 应用类 | classpath |
双亲委派保证安全性,特殊场景(SPI、热部署)需要破坏委派模型。