面试必备:JVM 类加载机制深度解析
类加载的完整过程
源代码 (.java)
↓ 编译
字节码文件 (.class)
↓ 类加载
Class 对象(存在于方法区/元空间)
↓
运行时使用
完整七步
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
↓ ↓ ↓ ↓ ↓
① ② ③ ④ ⑤
第一步:加载(Loading)
做什么:
- 通过类的全限定名获取类的二进制字节流
- 把字节流转化为方法区的运行时数据结构
- 在堆中生成
java.lang.Class对象
类的来源:
- 本地磁盘的 .class 文件
- JAR 包
- 网络下载
- 动态生成(ASM、CGlib)
- 数据库
第二步:验证(Verification)
确保字节码是安全的、正确的。
验证阶段检查:
├── 文件格式验证 → 魔数、版本号
├── 元数据验证 → 语法检查(是否有父类、是否继承了 final 类)
├── 字节码验证 → 指令是否正确(不会跳到方法外)
└── 符号引用验证 → 引用的类/字段/方法是否存在
第三步:准备(Preparation)
给类变量(static)分配内存并设置零值。
public class User {
static int count = 10; // 在准备阶段:count = 0
static final int MAX = 100; // 常量:在准备阶段:MAX = 100(编译期确定)
}
注意:
static int:初始值是 0,不是 10static final int:初始值是 100(编译时就确定)
第四步:解析(Resolution)
把符号引用替换为直接引用。
// 符号引用(编译时不知道实际地址)
"Lcom/User;.name" → Ljava/lang/String;
"sayHello()" → method ref sayHello
// 直接引用(运行时知道内存地址)
#1 = String @0x1234
#2 = Method @0x5678
第五步:初始化(Initialization)
执行 <clinit> 构造器,执行静态变量的赋值和静态代码块。
public class User {
static int a = 10; // ①
static {
a = 20; // ②
}
}
// 初始化顺序:a = 10 → a = 20 → a = 20
第六步 & 第七步:使用 & 卸载
正常使用,直到类不再被需要时被卸载。
类初始化的时机(主动引用 vs 被动引用)
主动引用(会立即触发初始化)
// ① new 对象
new User();
// ② 访问类的静态变量(不是 final)
int a = User.count; // 触发
// ③ 调用类的静态方法
User.say(); // 触发
// ④ 反射
Class.forName("com.User");
// ⑤ 初始化子类,父类先初始化
class Son extends User { } // User 先初始化
// ⑥ 启动类(main 方法所在类)
public class Test {
public static void main(String[] args) { }
}
被动引用(不触发初始化)
// ① 访问 static final 常量(编译期确定)
int b = User.MAX; // MAX 是 static final,不会触发
// ② 通过数组定义
User[] users = new User[10]; // 不会触发
// ③ 子类访问父类的 static 字段
class Son extends User {
static {
System.out.println("Son 初始化"); // 不执行
}
}
Son.count; // 触发的是 User,不是 Son
双亲委派模型(重点!)
什么是双亲委派?
当类加载器收到加载请求时,先让父类加载器处理,父类处理不了才自己处理。
类加载请求
│
▼
┌─────────────────────────────────────────┐
│ Bootstrap ClassLoader │ ← C++ 实现,无法访问
│ (启动类加载器) │
│ JAVA_HOME/jre/lib/*.jar │
└─────────────────────────────────────────┘
↑ 委派
│
┌─────────────────────────────────────────┐
│ Extension ClassLoader │
│ (扩展类加载器) │
│ JAVA_HOME/jre/lib/ext/*.jar │
└─────────────────────────────────────────┘
↑ 委派
│
┌─────────────────────────────────────────┐
│ Application ClassLoader │
│ (应用程序类加载器) │
│ classpath 中的类 │
└─────────────────────────────────────────┘
↑ 委派
│
┌─────────────────────────────────────────┐
│ Custom ClassLoader(自定义) │
│ (自定义类加载器) │
└─────────────────────────────────────────┘
工作流程(伪代码)
class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve) {
// ① 先检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// ② 委托给父类加载器
try {
ClassLoader parent = this.parent;
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 没有父类,委托给 Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
// ③ 父类加载不了,自己加载
if (c == null) {
c = findClass(name);
}
return c;
}
}
为什么需要双亲委派?
核心目的:安全 + 防止重复加载
场景:用户自定义了一个 java.lang.String
如果没有双亲委派:
├─ ApplicationClassLoader 加载
├─ ExtensionClassLoader 加载
├─ BootstrapClassLoader 加载
└─ 三种不同的 String 类存在!
如果有双亲委派:
├─ Application → 委托给父
├─ Extension → 委托给父
├─ Bootstrap → 加载 rt.jar 中的 String(官方版)
└─ 用户自定义的 String 永远不会被加载!
三种系统类加载器
| 加载器 | 负责目录 | 优先级 |
|---|---|---|
| Bootstrap | jre/lib | 最高 |
| Extension | jre/lib/ext | 中 |
| Application | classpath | 最低 |
自定义类加载器
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) {
// 读取 .class 文件
String path = "D:/classes/" + name.replace('.', '/') + ".class";
byte[] data = Files.readAllBytes(Paths.get(path));
// 调用父类方法定义类
return defineClass(name, data, 0, data.length);
}
}
// 使用
ClassLoader loader = new MyClassLoader();
Class<?> clazz = loader.loadClass("com.example.User");
使用场景
- 热部署(Tomcat)
- 加密保护(.class 文件加密)
- 动态生成类(ORM 框架)
面试高频问题
Q1:为什么重写 loadClass 就不走双亲委派了?
// 如果自定义类加载器重写了这个方法
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 直接自己加载,不委托父类
return findClass(name);
}
则每个类都会尝试自己加载,破坏了双亲委派,安全机制失效。
Q2:Tomcat 为什么打破双亲委派?
Tomcat 需求:
├── 部署多个 web 应用
├── 每个应用可能使用不同版本的同名类
└── 隔离不同应用的类
Tomcat 的类加载顺序(打破双亲委派):
- Bootstrap → Extension → common(双亲委派)
- WebApp1 → WebApp2(每个应用独立加载)
Q3:类的卸载条件?
一个类被卸载需要满足:
- 该类所有实例已被回收
- 该类的 ClassLoader 已被回收
- 该类的 Class 对象不再被引用
总结
类加载过程:加载 → 验证 → 准备 → 解析 → 初始化
双亲委派:先让父类加载,父类处理不了才自己加载
目的:安全 + 防止类重复加载
打破场景:Tomcat 热部署、自定义加密类加载
加载器层次:
Bootstrap → Extension → Application → 自定义
高优先级 ←————————————————→ 低优先级