让我用一个有趣的故事来解释JVM类加载过程!
故事开始:小明的新任务
想象一下,小明是一位Java程序员,他接到了一个新任务:创建一个Student
类。让我们跟随这个类的"一生",看看它是如何被JVM加载和使用的。
java
// Student.java - 小明的代码
public class Student {
// 静态变量 - 就像班级的公告栏
public static String schoolName = "Java魔法学校";
private static int studentCount = 0;
// 静态代码块 - 班级的初始化仪式
static {
System.out.println("🎉 班级初始化开始!");
studentCount = 100;
System.out.println("当前学生人数:" + studentCount);
}
// 实例变量 - 每个学生独有的
private String name;
private int age;
// 构造方法 - 创建学生的魔法
public Student(String name, int age) {
this.name = name;
this.age = age;
studentCount++;
System.out.println("新学生报到:" + name);
}
// 实例方法
public void study() {
System.out.println(name + "正在努力学习Java魔法!");
}
// 静态方法
public static void printSchoolInfo() {
System.out.println("欢迎来到:" + schoolName);
}
}
JVM类加载的四个阶段
第一阶段:加载(Loading)- 找到班级花名册
想象JVM是一个学校的教务处,需要找到Student
类的"花名册"(.class文件)。
java
// 模拟类加载器的工作
class ClassLoaderAdventure {
public Class<?> loadClass(String className) {
System.out.println("🔍 开始寻找类:" + className);
// 1. 通过全限定名获取二进制字节流
byte[] classBytes = findClassBytes(className);
// 2. 将字节流转换为方法区的运行时数据结构
Class<?> clazz = defineClass(className, classBytes);
// 3. 在堆中创建Class对象,作为访问方法区的入口
createClassObject(clazz);
System.out.println("✅ 类加载完成:" + className);
return clazz;
}
private byte[] findClassBytes(String className) {
// 这里会从文件系统、网络等地方查找.class文件
System.out.println("📁 正在从文件系统查找.class文件...");
return new byte[1024]; // 模拟.class文件内容
}
}
第二阶段:验证(Verification)- 检查花名册真伪
确保.class文件是安全、合规的:
java
class ClassVerifier {
public boolean verify(byte[] classBytes) {
System.out.println("🔎 开始验证类文件...");
// 1. 文件格式验证
if (!verifyFileFormat(classBytes)) {
System.out.println("❌ 文件格式错误!");
return false;
}
// 2. 元数据验证
if (!verifyMetadata(classBytes)) {
System.out.println("❌ 元数据验证失败!");
return false;
}
// 3. 字节码验证
if (!verifyBytecode(classBytes)) {
System.out.println("❌ 字节码验证失败!");
return false;
}
// 4. 符号引用验证
if (!verifySymbolicReferences(classBytes)) {
System.out.println("❌ 符号引用验证失败!");
return false;
}
System.out.println("✅ 类验证通过!");
return true;
}
}
第三阶段:准备(Preparation)- 准备教室设施
为静态变量分配内存并设置初始值:
java
class ClassPreparer {
public void prepare(Class<?> clazz) {
System.out.println("🛠️ 开始准备阶段...");
// 为静态变量分配内存并设置默认值
// schoolName = null (引用类型默认值)
// studentCount = 0 (int类型默认值)
System.out.println("📊 静态变量内存分配完成");
System.out.println(" schoolName = null");
System.out.println(" studentCount = 0");
}
}
第四阶段:解析(Resolution)- 翻译学生名单
将符号引用转换为直接引用:
java
class ClassResolver {
public void resolve(Class<?> clazz) {
System.out.println("🔗 开始解析阶段...");
// 将符号引用(如方法名、字段名)转换为直接的内存地址
resolveFields(clazz);
resolveMethods(clazz);
System.out.println("✅ 符号引用解析完成");
}
}
第五阶段:初始化(Initialization)- 班级正式成立
执行类构造器<clinit>()
方法,为静态变量赋真正的值:
java
class ClassInitializer {
public void initialize(Class<?> clazz) {
System.out.println("🚀 开始初始化阶段...");
// 执行静态代码块和静态变量赋值
// schoolName = "Java魔法学校"
// studentCount = 100
// 执行static {}代码块
System.out.println("✨ 静态代码块执行完成");
System.out.println(" schoolName = "Java魔法学校"");
System.out.println(" studentCount = 100");
}
}
完整的使用示例
java
public class ClassLoadingDemo {
public static void main(String[] args) {
System.out.println("🏁 程序开始执行");
// 第一次使用Student类,触发类加载
System.out.println("\n=== 第一次使用Student类 ===");
System.out.println("访问静态变量: " + Student.schoolName);
System.out.println("\n=== 创建Student实例 ===");
Student student1 = new Student("小明", 20);
student1.study();
System.out.println("\n=== 再次访问静态方法 ===");
Student.printSchoolInfo();
System.out.println("\n=== 创建第二个Student实例 ===");
Student student2 = new Student("小红", 19);
student2.study();
}
}
时序图:类加载的完整过程
运行结果预测
当你运行上面的代码时,你会看到:
text
🏁 程序开始执行

=== 第一次使用Student类 ===
🔍 开始寻找类:Student
📁 正在从文件系统查找.class文件...
✅ 类加载完成:Student
🔎 开始验证类文件...
✅ 类验证通过!
🛠️ 开始准备阶段...
📊 静态变量内存分配完成
schoolName = null
studentCount = 0
🔗 开始解析阶段...
✅ 符号引用解析完成
🚀 开始初始化阶段...
🎉 班级初始化开始!
当前学生人数:100
✨ 静态代码块执行完成
schoolName = "Java魔法学校"
studentCount = 100
访问静态变量: Java魔法学校
=== 创建Student实例 ===
新学生报到:小明
小明正在努力学习Java魔法!
=== 再次访问静态方法 ===
欢迎来到:Java魔法学校
=== 创建第二个Student实例 ===
新学生报到:小红
小红正在努力学习Java魔法!
重要知识点总结
- 加载时机:不是程序启动时加载所有类,而是第一次使用时才加载
- 双亲委派模型:类加载器会先委托父加载器尝试加载
- 初始化顺序:父类静态 → 子类静态 → 父类构造 → 子类构造
- 被动引用:有些情况不会触发初始化(如访问静态常量)
- 唯一性:同一个类在不同类加载器加载时,会被视为不同的类
通过这个故事,你应该对JVM类加载过程有了直观的理解。记住,类加载就像是Java类的"出生证明",确保每个类都能安全、正确地来到JVM世界中!