Skip to content

类的主动使用 vs 被动使用

区别只有一点:是否触发初始化

理解主动使用和被动使用的区别,是掌握类加载时机最关键的一环。

主动使用:会触发类的初始化(<clinit> 执行)。

被动使用:不会触发初始化(类可能已加载,但 <clinit> 不会执行)。

这个区别在面试中经常出现,但实际开发中更容易踩坑。

六种主动使用

JVM 规范定义了触发初始化的六种「首次主动使用」场景:

java
public class ActiveUse {
    static { System.out.println("ActiveUse initialized"); }

    // 场景 1:new 对象
    public static void test1() {
        new ActiveUse();  // 主动使用
    }

    // 场景 2:访问类的静态字段
    public static void test2() {
        int x = ActiveUse.COUNT;  // 主动使用
    }

    // 场景 3:调用类的静态方法
    public static void test3() {
        ActiveUse.doSomething();  // 主动使用
    }

    // 场景 4:反射
    public static void test4() throws Exception {
        Class.forName("com.example.ActiveUse");  // 主动使用
    }

    // 场景 5:初始化子类(父类也会被初始化)
    public static void test5() {
        new ActiveUseChild();  // 主动使用 Child + Parent
    }

    // 场景 6:主类(JVM 启动时自动初始化)
    public static void main(String[] args) {
        // 主类在 main() 执行前已被初始化
    }
}

class ActiveUseChild extends ActiveUse {}

被动使用:不触发初始化

场景一:引用子类的静态字段(父类被初始化,子类不)

这是最容易出错的地方:

java
class Parent {
    static int value = 100;
    static { System.out.println("Parent <clinit> executed"); }
}

class Child extends Parent {
    static int childValue = 200;
    static { System.out.println("Child <clinit> executed"); }
}

public class PassiveUseDemo {
    public static void main(String[] args) {
        System.out.println(Child.value);
    }
}

输出

Parent <clinit> executed
100

解释Child.value 实际上解析到了 Parent.value,所以只初始化了 ParentChild 类仍然处于「加载但未初始化」状态。

场景二:数组类型

java
class ArrayDemo {
    static { System.out.println("ArrayDemo initialized"); }
}

public class PassiveUseArray {
    public static void main(String[] args) {
        // 创建数组不触发类的初始化
        ArrayDemo[] arr = new ArrayDemo[10];
        System.out.println("Array created");
        // 输出:
        // Array created
        // (没有 "ArrayDemo initialized")
    }
}

new ArrayDemo[10] 创建的是数组类[Lcom.example.ArrayDemo;),而不是 ArrayDemo 类本身。数组类由 JVM 直接构造,不走类加载器的加载流程。

场景三:访问编译期常量

java
class Constants {
    // 编译期常量
    static final int CONST = 100;
    // 非编译期常量
    static final String RUNTIME = new String("hello");

    static { System.out.println("Constants initialized"); }
}

public class PassiveUseConst {
    public static void main(String[] args) {
        System.out.println(Constants.CONST);     // 不触发
        System.out.println(Constants.RUNTIME);   // 触发!
    }
}

CONST = 100 在编译时就被解析成字面量 100,编译后的字节码里根本没有对 Constants.CONST 的引用——只有 bipush 100。所以不需要初始化 Constants

RUNTIME = new String("hello") 不是编译期常量,必须在运行时求值,所以会触发初始化

场景四:定义 Class 对象

java
class ClassDemo {
    static { System.out.println("ClassDemo initialized"); }
}

public class PassiveUseClass {
    public static void main(String[] args) {
        // 获取 Class 对象不触发初始化
        Class&lt;?&gt; clazz = ClassDemo.class;
        System.out.println("Class object obtained");
        // 输出:
        // Class object obtained
        // (没有 "ClassDemo initialized")
    }
}

ClassDemo.class 属于「引用」而不是「主动使用」,只触发了加载阶段的「解析」,但没有触发 <clinit> 的执行。

一张图总结

引用类

    ├── 主动使用 ──────────────────┐
    │   触发 Initialization(<clinit>) │
    │                              │
    │   1. new 对象                 │
    │   2. 访问非 final static 字段  │
    │   3. 调用 static 方法         │
    │   4. 反射(Class.forName)    │
    │   5. 初始化子类(父类先初始化) │
    │   6. 主类(main 方法所在类)   │
    │                              │
    └── 被动使用                    │
        不触发 Initialization       │

        1. 子类引用父类 static 字段  │
           (只初始化父类)          │
        2. 定义 Class 对象           │
           (.class 不触发)          │
        3. 创建数组                  │
           (数组类是 JVM 自建的)    │
        4. 访问编译期常量(static final)│
           (编译时内联)            │

        ← ← ← ← ← ← ← ← ← ← ← ← ← ←

实战避坑

坑一:静态常量的默认值

java
class Config {
    // 这不是编译期常量,会触发初始化
    static final int TIMEOUT = Integer.parseInt(
        System.getProperty("timeout", "30"));

    static { System.out.println("Config initialized"); }
}

// 访问 TIMEOUT → 触发 Config 初始化
// 因为 parseInt() 不是编译期常量
// 字节码中仍然有对 Config.TIMEOUT 的引用

坑二:枚举类的初始化

枚举类的 <clinit> 在第一次访问任何枚举常量时执行:

java
enum Color {
    RED, GREEN, BLUE;

    static { System.out.println("Color enum initialized"); }
}

public class EnumInit {
    public static void main(String[] args) {
        System.out.println(Color.RED);  // 触发
        System.out.println(Color.valueOf("RED"));  // 不触发新初始化
        // 输出一次 "Color enum initialized"
    }
}

坑三:Lambda 表达式与类加载

Lambda 表达式的实现依赖于 invokedynamic 指令和 LambdaMetafactory。Lambda 表达式不会触发其引用的类的初始化(通常只是加载):

java
class HeavyClass {
    static { System.out.println("HeavyClass initialized"); }
}

public class LambdaInit {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("Lambda");
        // 访问 HeavyClass.class 不触发初始化
        // Lambda 表达式在运行时通过 invokedynamic 生成
    }
}

本节小结

主动使用 vs 被动使用,关键在于是否触发 <clinit> 的执行:

行为是否初始化
new 对象✅ 是
访问非 final static 字段✅ 是
调用 static 方法✅ 是
反射 Class.forName()✅ 是
初始化子类✅ 是(父先于子)
主类(main)✅ 是
子类引用父类 static 字段⚠️ 只初始化父类
ClassName.class❌ 否(只加载/解析)
创建数组❌ 否(不加载元素类)
访问编译期常量❌ 否(编译时内联)

理解了这个区别,就能准确预测类的初始化时机,也能理解为什么某些代码行为不符合直觉。

到这里,「类加载器与类加载过程」全部章节就完成了。接下来进入 PC 寄存器(概述/面试题)

基于 VitePress 构建