类加载机制
核心概念
Java 类加载器负责将
.class字节码加载到 JVM 中并转化为 Class 对象,整个过程分为「加载 → 链接(验证+准备+解析)→ 初始化」,并采用「双亲委派」模型来保证类唯一性、安全性与高效性。
双亲委派模型: Java 中的类加载器形成一棵树。每个类加载器在加载类时,不是自己先找,而是先把请求交给父加载器;只有当所有父加载器都表示“我找不到”时,它才自己去从磁盘/网络等位置查找并加载字节码。这个“层层往上托关系”的流程,就叫双亲委派模型。 这样可以保证:
- JDK 核心类只能由顶层的 BootstrapClassLoader 加载,防止用户篡改;
- 同一个类(同一 ClassLoader + 全限定名)在 JVM 中只有一个版本,保证类型唯一;
- 类一旦被父加载器加载过,子加载器就不会重复加载,提升性能。
🎯 核心判定规则:两个类相等的充要条件 = 同一个类加载器 + 完全相同的全限定名
一、为什么需要类加载机制与双亲委派?
1.1 问题背景(没有双亲委派会怎样)
- 唯一性破坏:同一个类被不同类加载器加载会产生多个类型,
instanceof和类型转换失效 - 安全隐患:用户可以随意编写
java.lang.String并被加载,篡改核心类库 - 性能浪费:每次都从磁盘读取字节码、重复验证、重复加载
1.2 设计动机
- 安全性:保证 JVM 核心类库安全(
java.*包只能由 Bootstrap 加载) - 避免重复:同一个类只加载一次(缓存机制)
- 职责分离:实现类加载的优先级与分工协作
二、类加载全过程详解
2.1 生命周期七阶段
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
└────────链接────────┘
1️⃣ 加载(Loading)
- 通过全限定名查找字节码文件(来源:文件系统/jar/网络/动态生成)
- 将字节码读入方法区(JDK8 后是元空间 Metaspace)
- 在堆中生成对应的
java.lang.Class对象作为方法区数据的访问入口
2️⃣ 链接(Linking)
| 阶段 | 作用 | 典型错误 |
|---|---|---|
| 验证 | 检查字节码安全性、魔数 0xCAFEBABE、版本兼容性 |
VerifyError |
| 准备 | 为静态变量分配内存,赋默认值(int→0, Object→null) ⚠️ final static 常量直接赋初始值 |
- |
| 解析 | 将符号引用转为直接引用(内存地址) 可选阶段,支持动态绑定时延迟解析 | NoSuchMethodError |
关键区别:
public static int value = 123; // 准备阶段:value=0,初始化阶段:value=123
public static final int VALUE = 123; // 准备阶段:VALUE=123(编译期常量)
3️⃣ 初始化(Initialization)
执行 <clinit>() 方法(静态变量赋值 + static 块,按代码顺序执行)
触发初始化的 6 种时机(主动引用):
new创建对象- 访问/修改静态变量(被 final 修饰的常量除外)
- 调用静态方法
- 反射调用
Class.forName("com.test.User") - 初始化子类会先触发父类初始化
- 启动类(包含
main方法的类)
不会触发初始化(被动引用):
// ❌ 通过子类引用父类静态字段
System.out.println(Child.parentValue);
// ❌ 通过数组定义引用类
Parent[] arr = new Parent[10];
// ❌ 引用常量(编译期常量折叠)
System.out.println(Config.CONSTANT);
2.2 完整工作流程图
请求加载类:com.example.User
↓
1. findLoadedClass() ← 检查是否已加载(缓存)
↓ 未加载
2. 【双亲委派】parent.loadClass() ← 委托给父加载器
↓
Bootstrap → Extension → Application → Custom
↓ 逐层向上委托
↓ 父加载器无法加载(ClassNotFoundException)
3. findClass() ← 当前加载器自己加载
↓
4. defineClass() ← 将字节码转为 Class 对象
↓
5. 链接:验证 → 准备 → 解析
↓
6. 初始化:执行 <clinit>()
↓
返回 Class 对象
三、双亲委派模型详解
3.1 三层默认类加载器(JDK 8)
| 加载器名称 | 加载路径 | 父加载器 | 可被程序获取 | Java 实现 |
|---|---|---|---|---|
| Bootstrap ClassLoader | $JAVA_HOME/jre/lib/rt.jar 核心类库(String、Object) |
无 | ❌ 返回 null |
C++ 实现 |
| Extension ClassLoader | $JAVA_HOME/jre/lib/ext/*.jar |
Bootstrap | ✅ | sun.misc.Launcher$ExtClassLoader |
| Application ClassLoader | classpath 指定路径 |
Extension | ✅ ClassLoader.getSystemClassLoader() |
sun.misc.Launcher$AppClassLoader |
| Custom ClassLoader | 自定义路径 | Application | ✅ | 用户自定义 |
JDK 9+ 变化:
- 模块化后,Extension 被 Platform ClassLoader 取代
- 加载路径改为模块路径(
--module-path)
3.2 源码分析(java.lang.ClassLoader.loadClass)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 【步骤1】先检查是否已经加载过(缓存机制)
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 【步骤2】双亲委派:先让父加载器加载
c = parent.loadClass(name, false);
} else {
// 【步骤3】parent=null 说明到顶层,交给 Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器加载失败,不处理异常
}
if (c == null) {
// 【步骤4】父加载器都无法加载,自己尝试加载
c = findClass(name); // ⭐ 自定义加载器重写此方法
}
}
// resolve=true 时进行链接中的解析阶段
if (resolve) {
resolveClass(c);
}
return c;
}
}
关键方法对比:
| 方法 | 作用 | 是否遵循双亲委派 | 自定义时是否重写 |
|---|---|---|---|
loadClass() |
完整加载流程(包含双亲委派逻辑) | ✅ | ❌ 不推荐 |
findClass() |
实际查找字节码的逻辑 | - | ✅ 推荐重写 |
defineClass() |
将字节数组转为 Class 对象 | - | ❌ final 方法 |
3.3 典型案例
// 案例1:核心类由 Bootstrap 加载
Class<?> c1 = String.class;
System.out.println(c1.getClassLoader()); // null(代表 Bootstrap)
// 案例2:自定义 java.lang.String 会加载失败
// java.lang.SecurityException: Prohibited package name: java.lang
public class java.lang.String { } // ⚠️ 包名保护机制
// 案例3:验证类加载器层级关系
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
ClassLoader extLoader = appLoader.getParent();
ClassLoader bootLoader = extLoader.getParent();
System.out.println("App: " + appLoader); // sun.misc.Launcher$AppClassLoader
System.out.println("Ext: " + extLoader); // sun.misc.Launcher$ExtClassLoader
System.out.println("Boot: " + bootLoader); // null
// 案例4:打破双亲委派(SPI 机制)
DriverManager.class.getClassLoader(); // null(Bootstrap 加载)
com.mysql.jdbc.Driver.class.getClassLoader(); // AppClassLoader 加载
// 通过 ThreadContextClassLoader 实现跨加载器调用
四、如何破坏/自定义双亲委派?
4.1 三种常见方式
| 方式 | 实现方式 | 典型场景 | 原理 |
|---|---|---|---|
| 自定义 ClassLoader | 重写 loadClass() 破坏委托逻辑 |
Tomcat、Spring Boot | 先尝试自己加载,再委托父加载器 |
| Thread ContextClassLoader | Thread.currentThread().setContextClassLoader() |
JDBC、JNDI、SPI | 父加载器调用子加载器 |
| 热替换/热部署 | 创建新 ClassLoader 丢弃旧的 | HotSwap、OSGi | 同名类不同加载器=不同类 |
4.2 Tomcat 类加载器结构(破坏双亲委派)
Bootstrap ClassLoader
↓
Extension ClassLoader
↓
Application ClassLoader
↓
Common ClassLoader ← Tomcat/lib(所有 Web 应用共享)
├─ Catalina ClassLoader ← Tomcat 服务器内部类
├─ Shared ClassLoader ← 多个 Web 应用共享
└─ WebappClassLoader1 ← /WEB-INF/classes & lib(应用隔离)
WebappClassLoader2 ← 不同版本的同一个库可以共存
Tomcat 加载顺序(破坏双亲委派):
- Bootstrap/Extension 加载的类
- 先加载
/WEB-INF/classes(优先使用应用自己的类) - 再加载
/WEB-INF/lib/*.jar - 最后才委托给父加载器
为什么这样设计?
- 🔧 隔离性:不同 Web 应用使用不同版本的 Spring 互不影响
- 🔄 灵活性:支持热部署,只需替换对应的 WebappClassLoader
- ⚡ 性能:避免重复加载同一个库
4.3 SPI 机制(Service Provider Interface)
问题:DriverManager 由 Bootstrap 加载,如何加载 classpath 下的 MySQL 驱动?
解决方案:线程上下文类加载器
// DriverManager 源码(精简版)
static {
loadInitialDrivers(); // 静态块自动执行
}
private static void loadInitialDrivers() {
// 获取线程上下文类加载器(默认是 AppClassLoader)
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next(); // 触发 com.mysql.jdbc.Driver 加载
}
}
关键代码:
// 设置线程上下文类加载器
Thread.currentThread().setContextClassLoader(appClassLoader);
// ServiceLoader 内部使用上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class.forName("com.mysql.jdbc.Driver", false, cl);
五、类加载器相关面试高频题
Q1:双亲委派模型的作用是什么?
答:
- 避免类重复加载:同一类只加载一次,通过
findLoadedClass()缓存 - 保证核心 API 不被篡改:
java.lang.String只能由 Bootstrap 加载 - 保证类型唯一性:同一个 ClassLoader + 全限定名 → 唯一的 Class 对象
Q2:为什么自定义类加载器需要重写 findClass 而不是 loadClass?
答:
- 重写
loadClass会彻底破坏双亲委派机制,失去安全保障 - 推荐只重写
findClass,保留双亲委派的好处 loadClass已经实现了完整的委派逻辑,只需实现"如何查找字节码"的细节
Q3:Class.forName() 和 ClassLoader.loadClass() 的区别?
| 区别点 | Class.forName() |
ClassLoader.loadClass() |
|---|---|---|
| 是否初始化 | 默认执行静态块(可传 initialize=false) |
不执行初始化(只加载+链接) |
| 使用加载器 | 当前类的加载器(可指定) | 指定 ClassLoader |
| 典型用途 | JDBC 驱动加载、Spring IOC | 热加载、插件系统、不想触发初始化 |
| 方法签名 | Class.forName(String name, boolean initialize, ClassLoader loader) |
💬 评论