JVM 类加载机制复习

一、类加载的整体流程(7 阶段)

根据《Java 虚拟机规范》,一个类从 .class 文件到可被 JVM 使用,需经历以下 7 个阶段

复制代码
加载(Loading)
  ↓
验证(Verification)
  ↓
准备(Preparation)
  ↓
解析(Resolution)
  ↓
初始化(Initialization)
  ↓
使用(Using)
  ↓
卸载(Unloading)

其中前 5 步属于 类加载过程(Class Loading Process),也是我们重点分析的部分。

⚠️ 注意:"加载" ≠ "初始化"!很多面试者混淆这两个概念。

二、逐阶段详解 + 代码验证

第 1 阶段:加载(Loading)

✅ 做了什么?
  1. 通过类的全限定名 (如 com.example.Foo)获取其二进制字节流(.class 文件内容)。
    • 来源可以是:
      • 本地文件系统(最常见)
      • 网络(如 RMI、Applet)
      • 数据库(少见)
      • 动态生成(如 ASM、CGLIB、Lambda 表达式)
  2. 将字节流解析为 JVM 内部数据结构(存入方法区 / Metaspace)。
  3. 堆中创建一个 java.lang.Class 对象,作为程序访问该类的入口。
🔧 谁负责?
  • ClassLoader 子类 (如 AppClassLoader、自定义 ClassLoader)
  • Bootstrap ClassLoader(由 C++ 实现,无法在 Java 中直接引用)
📌 示例:观察"加载"是否发生
java 复制代码
public class LoadDemo {
    static {
        System.out.println("LoadDemo 被初始化了!");
    }
}
java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        // 方式1:仅加载,不初始化
        Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("LoadDemo");
        System.out.println("类已加载,但未初始化");

        // 方式2:触发初始化
        Class.forName("LoadDemo"); // 默认 initialize=true
    }
}

输出:

复制代码
类已加载,但未初始化
LoadDemo 被初始化了!

结论

  • loadClass() 只执行到"加载"阶段(可能包含验证、准备),不会初始化
  • Class.forName() 默认会执行到"初始化"阶段。

第 2 阶段:验证(Verification)

✅ 做了什么?

确保字节码安全、合法、符合 JVM 规范,防止恶意代码破坏 JVM。分为四步:

子阶段 作用
文件格式验证 检查魔数(CAFEBABE)、版本号、常量池等是否合法
元数据验证 检查类结构(如继承关系、final 类是否被继承)
字节码验证 检查指令是否合法(如操作数栈溢出、类型不匹配)
符号引用验证 确保解析阶段能正确找到目标类/方法/字段

💡 验证失败会抛出 VerifyError(属于 LinkageError)。

📌 为什么需要?
  • 安全性:防止伪造的 .class 文件破坏 JVM。
  • 稳定性:避免运行时崩溃(如非法跳转指令)。

⚠️ 开发中极少遇到,除非手动修改字节码或使用不兼容的编译器。

第 3 阶段:准备(Preparation)

✅ 做了什么?
  • 类变量(static 字段) 分配内存(在方法区)。
  • 设置初始值(零值)不是赋值语句的值
📌 关键规则:
字段类型 初始值
int / long 0 / 0L
boolean false
引用类型 null
static final 且为编译期常量 直接赋值(因为值已存入常量池)
🧪 代码验证:
java 复制代码
public class PreparationDemo {
    public static int a = 100;           // 准备阶段设为 0,初始化阶段才设为 100
    public static final int b = 200;     // 编译期常量,准备阶段直接设为 200
    public static final String c = "OK"; // 同上
    public static String d = "Hello";    // 准备阶段为 null,初始化阶段为 "Hello"
}
java 复制代码
public class TestPrep {
    public static void main(String[] args) throws Exception {
        // 仅触发加载+准备,不初始化
        Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("PreparationDemo");

        // 通过反射读取字段(注意:反射会触发初始化!)
        // 所以不能用反射验证准备阶段的值!

        // 正确方式:用 HSDB 或字节码工具查看,但面试中只需理解逻辑
        System.out.println("准备阶段完成");
    }
}

面试重点

static int a = 100; 在准备阶段的值是 0,不是 100!

第 4 阶段:解析(Resolution)

✅ 做了什么?

符号引用(Symbolic Reference) 转换为直接引用(Direct Reference)

  • 符号引用 :如 "java/lang/String.valueOf:(I)Ljava/lang/String;"(字符串形式)
  • 直接引用:如方法在内存中的地址、偏移量
📌 解析时机:
  • 静态解析 :编译期可知的(如 invokespecialinvokestatic)------在解析阶段完成。
  • 动态解析 :运行时才知道的(如 invokevirtual)------延迟到真正调用时(方法表查找)。

所以解析不一定在初始化前完成!这是很多人误解的点。

第 5 阶段:初始化(Initialization)

✅ 做了什么?

执行类的 <clinit> 方法(Class Initializer),包括:

  • 所有 static {} 静态代码块
  • 所有 static 字段的显式赋值语句

<clinit> 是 JVM 自动生成的,程序员无法直接编写。

📌 初始化顺序:
  1. 父类先于子类初始化
  2. static 字段和 static{}代码顺序执行
🧪 代码演示:
java 复制代码
class Parent {
    static int p = 1;
    static {
        System.out.println("Parent static block, p=" + p);
        p = 2;
    }
}

class Child extends Parent {
    static int c = 3;
    static {
        System.out.println("Child static block, c=" + c);
        c = 4;
    }
}

public class InitOrder {
    public static void main(String[] args) {
        new Child(); // 触发初始化
    }
}

输出:

复制代码
Parent static block, p=1
Child static block, c=3

说明 :父类先初始化,且 static 赋值按代码顺序执行。

三、什么情况下会触发"初始化"?(7 种主动使用)

JVM 规范明确规定,只有以下 7 种情况 会触发类的初始化(从而执行 <clinit>):

  1. new 创建对象(new MyClass()
  2. 调用类的静态方法
  3. 访问类的非 final 静态字段
  4. 使用反射(如 Class.forName("MyClass")
  5. 初始化一个类时,其父类尚未初始化
  6. 启动类(包含 main 方法的类)
  7. JDK 1.7+ 的 MethodHandle 解析(动态调用)

❌ 被动使用不会触发初始化:

  • 子类引用父类的静态字段(只初始化父类)
  • 定义数组:MyClass[] arr = new MyClass[10];
  • 访问 static final 编译期常量

四、类加载器与双亲委派模型

类加载器层级:

加载器 加载路径 特点
Bootstrap ClassLoader $JAVA_HOME/jre/lib(如 rt.jar) C++ 实现,无法在 Java 中获取
Extension ClassLoader $JAVA_HOME/jre/lib/ext 加载扩展库
Application ClassLoader -classpath-cp 指定路径 加载应用程序类
Custom ClassLoader 自定义路径 如 Tomcat 的 WebAppClassLoader

双亲委派工作流程:

java 复制代码
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 {
                    // 3. 顶层用 Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ignore
            }
            if (c == null) {
                // 4. 父加载器无法加载,自己尝试
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

为什么用双亲委派?

  • 安全性 :防止用户自定义 java.lang.String 替换核心类。
  • 唯一性:确保核心类全局唯一,避免类冲突。
  • 避免重复加载:同一个类只会被加载一次。

五、高级面试题精讲

Q1:Class.forName("X")ClassLoader.loadClass("X") 有什么区别?

方法 是否初始化 是否触发 <clinit>
Class.forName("X") (默认)
ClassLoader.loadClass("X")

可通过 Class.forName(name, initialize, loader) 控制是否初始化。

Q2:如何打破双亲委派?为什么要打破?

  • 打破方式 :重写 loadClass() 方法,先自己加载,再委托父类。
  • 典型场景
    • JDBCDriverManager 使用 Thread.currentThread().getContextClassLoader() 加载驱动(SPI 机制)。
    • Tomcat:每个 Web 应用有自己的 ClassLoader,实现应用隔离。

Q3:类什么时候会被卸载?

  • 条件(必须同时满足):
    1. 该类的所有实例都已被回收
    2. 加载该类的 ClassLoader 已被回收
    3. 该类的 java.lang.Class 对象没有被任何地方引用
  • 通常只在 自定义 ClassLoader + 动态加载 场景下发生(如 OSGi、热部署)。

六、总结:类加载机制的核心价值

阶段 核心作用 面试关键词
加载 获取字节码,生成 Class 对象 ClassLoader、双亲委派
验证 保证字节码安全合法 VerifyError
准备 static 字段分配内存 + 零值 零值 vs 赋值
解析 符号引用 → 直接引用 静态解析 vs 动态分派
初始化 执行 <clinit> 主动使用、7 种触发条件
相关推荐
飞火流星020273 小时前
【Arthas工具】使用Trace命令分析Java JVM方法调用链路及耗时
java·jvm·arthas·jvm性能调优·java方法调用链路分析及耗时·jvm实时分析·jvm方法调用实时分析
7ioik3 小时前
JVM 调优工具深度指南:从监控到诊断的全流程实战
jvm
喵手4 小时前
JVM 基础知识:深入理解 Java 的运行时环境!
java·jvm·jvm基础·java运行环境
WizLC19 小时前
【JAVA】JVM类加载器知识笔记
java·jvm·笔记
CodeAmaz20 小时前
Java 垃圾回收(GC)算法详解
java·jvm·算法·垃圾回收算法
漫漫求1 天前
Java内存模型【JMM】、JVM内存模型
java·开发语言·jvm
dddaidai1231 天前
深入JVM(三):JVM执行引擎
java·jvm
小羊学伽瓦1 天前
ThreadLocal
java·jvm·算法
脸大是真的好~1 天前
JVM面试题相关-中级
jvm