# 46. JVM 的类加载过程是什么?有哪些关键步骤?

# 标准答案

JVM 的类加载过程分为 加载(Loading)、链接(Linking)、初始化(Initialization) 三个主要阶段。链接阶段又包含 验证(Verification)、准备(Preparation)、解析(Resolution)。整个过程确保类在运行时被正确加载、验证并准备使用。

# 答案解析

Java 采用 按需加载(Lazy Loading) 机制,只有在首次使用某个类时,JVM 才会触发类加载。类的生命周期包括:加载 -> 链接(验证、准备、解析)-> 初始化 -> 使用 -> 卸载

# 1. 加载(Loading)

  • 从字节码(Class 文件)获取类定义,可能来源于 JAR 包、文件系统、网络等
  • 生成 Class 对象,并存入 方法区(Metaspace),在堆中创建 java.lang.Class 实例进行管理。
  • 类加载器(ClassLoader)负责查找、转换和定义类,使用 双亲委派模型(Bootstrap -> Ext -> App -> Custom)。

# 2. 链接(Linking)

包含以下三个步骤:

  • 验证(Verification):检查 Class 文件格式是否符合 JVM 规范,防止恶意字节码攻击。
  • 准备(Preparation):为类的静态变量分配 默认初始值(非 final 变量),但不会执行静态代码块。
  • 解析(Resolution):将 符号引用(Symbolic Reference)转换为直接引用,如解析 MethodHandle、字段和方法调用。

# 3. 初始化(Initialization)

  • 执行静态变量赋值和静态代码块,按代码顺序执行。
  • 父类优先初始化,子类初始化前必须保证父类已完成初始化。
  • 只有在满足以下条件时才会触发类初始化:
    1. 首次主动使用该类(如 new、访问静态字段、调用静态方法)。
    2. 通过反射调用 Class.forName("XXX")
    3. 子类初始化时,父类必须先初始化
    4. Java 入口 main(String[] args) 所在的类

# 代码示例

class Parent {
    static int A = 10;
    static { System.out.println("Parent static block"); }
}

class Child extends Parent {
    static int B = 20;
    static { System.out.println("Child static block"); }
}

public class Test {
    public static void main(String[] args) {
        System.out.println(Child.B);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出顺序

Parent static block
Child static block
20
1
2
3

解析

  1. 访问 Child.B 时,JVM 先初始化 Parent,然后初始化 Child
  2. 静态代码块在类初始化阶段执行Parent 先执行,Child 后执行。

# 常见错误

  1. 误以为 Class.forName("X") 只是加载类,实际上它会触发类的 初始化(可用 ClassLoader.loadClass("X") 仅加载但不初始化)。
  2. 误解 static final 变量的初始化
    class A {
        static final int X = 100;
        static { System.out.println("Class A initialized"); }
    }
    public class Test {
        public static void main(String[] args) {
            System.out.println(A.X);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    不会触发 A 的初始化,因为 static final 变量在编译期就已存入常量池,不依赖类初始化。

# 最佳实践

  • 避免 Class.forName() 触发不必要的类初始化
  • 使用 ClassLoader.loadClass() 仅加载但不初始化类,提高性能。
  • 使用 -XX:+TraceClassLoading 监控类加载情况

# 深入追问

  1. 双亲委派机制的作用是什么?如何破坏它?

    • 作用:防止类被重复加载,保证 Java 核心 API 的安全性。
    • 破坏方式:自定义 ClassLoader,重写 findClass() 方法绕过 super.loadClass()
  2. 为什么 static final 变量不会触发类初始化?

    • 编译期常量优化,static final 变量值会直接存入常量池,无需类加载。
  3. JVM 何时卸载类?

    • ClassLoader 没有被任何对象引用,且其加载的所有类都不可达时,JVM 会卸载该类,释放 Metaspace 空间。

# 相关面试题

  • 类加载器(ClassLoader)如何工作?如何自定义类加载器?
  • 什么是双亲委派模型?如何破坏它?
  • 类加载过程中,哪些操作会触发类的初始化?