Java 类加载的过程是怎样的?

#Java#面试八股

Java 类加载的过程是怎样的?

结论先行:一个类从 .class 字节流到能用,要走加载 → 验证 → 准备 → 解析 → 初始化五个阶段,之后才是使用和卸载。前三个阶段由类加载器驱动,后两个阶段由 JVM 按「首次主动使用」的时机触发。其中准备阶段只分配内存并给零值,真正赋值发生在初始化阶段的 <clinit>。

一、五阶段速览

阶段做什么关键细节
加载(Loading)通过全限定名取字节流,生成 Class 对象字节流可来自文件、JAR、网络、动态生成
验证(Verification)文件格式、元数据、字节码、符号引用校验防止非法字节码破坏 JVM
准备(Preparation)为静态变量分配内存并设零值static int a = 1 此刻 a == 0
解析(Resolution)常量池符号引用替换为直接引用可延迟到首次使用时进行
初始化(Initialization)执行 <clinit>,赋真实值、跑静态块JVM 保证只执行一次且线程安全
.class 字节流
   │
   ├─ 加载:读取 → 生成 java.lang.Class 实例 → 放入方法区
   ├─ 验证:魔数 CAFEBABE → 版本号 → 元数据 → 字节码 → 符号引用
   ├─ 准备:静态字段分配内存,赋 0 / null / false
   ├─ 解析:CONSTANT_Class / Methodref → 内存地址
   └─ 初始化:<clinit> 按源码顺序执行静态赋值与静态块

二、准备阶段的零值陷阱

public class PrepareDemo {
    static int a = 1;                    // 准备阶段 a = 0,初始化阶段才变 1
    static final int B = 2;              // 编译期常量,准备阶段直接写入 2
    static final int C = "abc".length(); // 非编译期常量,仍在 <clinit> 中赋值
    static boolean flag = true;          // 准备阶段 false

    static {
        System.out.println("a=" + a + ", B=" + B + ", flag=" + flag);
    }

    public static void main(String[] args) {
        System.out.println("main: a=" + a);
    }
}
// 输出:
// a=0, B=2, flag=true
// main: a=1

静态块里打印出 a=0,正是因为静态块和 a = 1 同属 <clinit>,按源码顺序执行,此时赋值语句还没轮到。

三、初始化的六个时机

只有主动使用才触发初始化,且父类先于子类:

场景是否触发
new、读写静态字段、调用静态方法是
反射 Class.forName("X")是(默认 initialize=true)
初始化子类时,父类先初始化是
虚拟机启动时的主类(含 main)是
MethodHandle / VarHandle 解析到该类的静态成员是
通过子类引用父类静态字段否(只初始化父类)
定义该类型的数组 new X[0]否
引用编译期常量 static final否(已内联到调用方常量池)
class Parent {
    static { System.out.println("Parent init"); }
    static int value = 10;
}

class Child extends Parent {
    static { System.out.println("Child init"); }
}

public class PassiveRef {
    public static void main(String[] args) {
        Parent[] arr = new Parent[3];       // 不初始化
        System.out.println(Parent.value);   // 只初始化 Parent
        Child[] children = new Child[1];    // 仍不初始化 Child
    }
}
// 输出:
// Parent init
// 10

四、<clinit> 的线程安全

JVM 对 <clinit> 加锁:多线程同时初始化同一个类时,只有一个线程执行,其余阻塞等待,执行完再唤醒。这也意味着静态块里做耗时操作会拖住所有使用该类的线程,甚至造成死锁——两个类在各自的 <clinit> 里互相等待对方初始化完成,就是经典的类初始化死锁。

public class ClinitLock {
    static {
        try {
            Thread.sleep(2000);       // 静态块里做 I/O,其他线程全部卡住
        } catch (InterruptedException ignored) {
        }
    }

    public static void main(String[] args) {
        new Thread(() -> System.out.println("t1: " + System.currentTimeMillis())).start();
        new Thread(() -> System.out.println("t2: " + System.currentTimeMillis())).start();
    }
}

五、常见追问

追问回答要点
加载和初始化是一回事吗不是。加载把字节流变成 Class 对象,初始化执行 <clinit>;两者可能相隔很久
Class.forName 和 loadClass 区别前者默认初始化,后者只加载不初始化;JDBC 驱动用 forName 正是要触发静态块注册
接口有 <clinit> 吗有,但不要求先初始化父接口,只有真正用到父接口的常量时才初始化它
解析一定要在初始化前完成吗不必须。JVM 规范允许延迟解析,只要在首次使用符号引用前完成即可
数组类怎么加载数组类由 JVM 直接创建,元素类型由对应类加载器加载,数组类本身没有 <clinit>

小结

五个阶段里最容易被面试追问的是「准备赋零值、初始化赋真值」和「主动使用才初始化」。记住 <clinit> 只跑一次且带锁,静态块里就别放重活了。

笔记加载中…