第二章:类加载机制与类生命周期 —— 双亲委派模型到底解决了什么问题?
本章目标:掌握 JVM 类加载器体系、双亲委派模型、类加载生命周期,以及类初始化与卸载原理,为理解内存管理和类加载优化打基础。
一、为什么要讲类加载机制?
Java 是动态语言的一大特点:
- 类在 运行时 才会被加载
- JVM 可以根据需要延迟加载类
- 动态加载使 Java 支持 跨平台、插件化、热部署
例如:
Class<?> clazz = Class.forName("com.example.User");
Object obj = clazz.newInstance();
这段代码可以在运行时根据类名加载 User 类,而不是在编译时硬编码。
如果不理解类加载机制,你很难理解:
- JVM 内存分区
- 静态代码块执行时机
- 自定义类加载器原理
- ClassCastException 和类卸载问题
二、类加载器(ClassLoader)
JVM 的类加载是由 ClassLoader 完成的。
核心概念:
| 名称 | 作用 |
|---|---|
| Bootstrap ClassLoader | 启动类加载器,负责加载 JDK 核心类库(rt.jar) |
| Extension ClassLoader | 扩展类加载器,加载 JDK 扩展库(jre/lib/ext) |
| AppClassLoader | 应用类加载器,加载classpath下的用户类 |
| 自定义 ClassLoader | 用户自定义,可实现热加载、隔离等 |
执行流程示意图:
AppClassLoader
│
------------------
│ │
ExtensionClassLoader 自定义ClassLoader
│
BootstrapClassLoader
1. 双亲委派模型
定义
当类加载器收到类加载请求时,它 先把请求交给父加载器,父加载器加载不了再自己尝试加载。
流程:
ClassLoader.loadClass("com.example.User")
│
▼
父加载器
│
▼
尝试加载
│
└─> 成功?返回
└─> 失败?子加载器加载
为什么要双亲委派?
- 保证核心类安全
避免用户自定义类覆盖java.lang.String、java.lang.Object。 - 避免重复加载
如果每个 ClassLoader 都可以独立加载String,可能产生多个java.lang.String类对象。 - 提高效率
系统类先由父加载器加载,避免重复查找。
2. 类加载器示例
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception {
ClassLoader appClassLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println("AppClassLoader: " + appClassLoader);
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("ExtClassLoader: " + extClassLoader);
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("BootstrapClassLoader: " + bootstrapClassLoader);
}
}
输出示例:
AppClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
ExtClassLoader: sun.misc.Launcher$ExtClassLoader@1b6d3586
BootstrapClassLoader: null
注意:BootstrapClassLoader 是 C++实现的,所以输出为 null。
三、类加载生命周期
JVM 中一个类从 加载到卸载 的完整生命周期:
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)
1. 加载(Loading)
- 读取
.class文件 - 将二进制数据放入 方法区
- 生成 Class 对象(元数据)
Class<?> clazz = Class.forName("com.example.User");
此时触发 加载阶段。
2. 验证(Verification)
- 确保字节码合法
- 不会破坏 JVM 安全
- 验证类型、语法、堆栈安全
3. 准备(Preparation)
- 为类的 静态变量 分配内存
- 并设置 默认值
public class User {
static int count = 10;
static String name = "Tom";
}
count→ 0name→ null
注意:赋值为常量的静态变量可能会直接在准备阶段赋值。
4. 解析(Resolution)
- 将符号引用转换为直接引用
- 例如:方法调用、类字段引用都被解析
5. 初始化(Initialization)
- 执行类的 静态代码块 和静态变量赋值
public class User {
static int count = 10;
static {
System.out.println("User类初始化");
count = 20;
}
}
输出:
User类初始化
- 静态变量最终值为
20
初始化触发条件:
new创建对象- 调用静态方法
- 访问静态字段(非编译时常量)
Class.forName()显式初始化
6. 使用(Using)
类被加载、初始化完成后,程序可以正常使用。
User user = new User();
System.out.println(User.count); // 20
7. 卸载(Unloading)
- 当 ClassLoader 不再引用类,类及其实例可被 GC 回收
- 一般 自定义 ClassLoader 热加载时会用到
- JVM 内置类不会被卸载
四、双亲委派模型解决了什么问题?
总结如下:
| 问题 | 双亲委派解决方案 |
|---|---|
| 核心类被覆盖 | 父类优先加载,保证系统安全 |
| 类重复加载 | 父加载器先加载,子加载器不重复加载 |
| 系统安全 | 防止恶意用户定义 java.lang.* 类 |
五、类加载优化技巧(拓展)
- 自定义 ClassLoader
可以动态加载模块、插件
URLClassLoader loader = new URLClassLoader(urls);
Class<?> plugin = loader.loadClass("com.example.Plugin");
- 避免重复加载类
尽量使用父类加载器加载系统类库 - 延迟加载
用到类时才加载,减少启动时间
六、面试高频问题
- 双亲委派模型是什么?为什么要用?
答:父加载器优先加载,保证核心类安全和类唯一性。 - 类加载的五个阶段是什么?
答:加载 → 验证 → 准备 → 解析 → 初始化 - 什么时候类会被初始化?
答:new、调用静态方法、访问非编译期常量、Class.forName() - BootstrapClassLoader 为什么显示 null?
答:它是 JVM 本身实现的,不是 Java 对象。 - 类什么时候会卸载?
答:ClassLoader 不再引用该类,且类实例全部回收时。
七、本章总结
- 类加载器:负责加载类,支持跨平台和热部署
- 双亲委派模型:父先行加载,保证核心类安全
- 类生命周期:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
- 初始化触发条件:
new/ 静态方法 / 非编译期常量 /Class.forName() - 卸载与自定义 ClassLoader:用于插件化、热加载
下一章将深入 Java 内存模型 (JMM) 与运行时数据区,讲解:
- 堆、栈、方法区、程序计数器、线程共享/独占内存
- 线程安全与可见性原理
- 与类加载器和 GC 的关系
如果你愿意,我可以直接帮你写 第三章:Java 内存模型 (JMM) 与运行时数据区,把每个内存区、线程模型和可见性讲透。
你希望我直接写吗?