类的主动使用 vs 被动使用
区别只有一点:是否触发初始化
理解主动使用和被动使用的区别,是掌握类加载时机最关键的一环。
主动使用:会触发类的初始化(<clinit> 执行)。
被动使用:不会触发初始化(类可能已加载,但 <clinit> 不会执行)。
这个区别在面试中经常出现,但实际开发中更容易踩坑。
六种主动使用
JVM 规范定义了触发初始化的六种「首次主动使用」场景:
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 {}被动使用:不触发初始化
场景一:引用子类的静态字段(父类被初始化,子类不)
这是最容易出错的地方:
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,所以只初始化了 Parent。Child 类仍然处于「加载但未初始化」状态。
场景二:数组类型
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 直接构造,不走类加载器的加载流程。
场景三:访问编译期常量
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 对象
class ClassDemo {
static { System.out.println("ClassDemo initialized"); }
}
public class PassiveUseClass {
public static void main(String[] args) {
// 获取 Class 对象不触发初始化
Class<?> 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)│
(编译时内联) │
│
← ← ← ← ← ← ← ← ← ← ← ← ← ←实战避坑
坑一:静态常量的默认值
class Config {
// 这不是编译期常量,会触发初始化
static final int TIMEOUT = Integer.parseInt(
System.getProperty("timeout", "30"));
static { System.out.println("Config initialized"); }
}
// 访问 TIMEOUT → 触发 Config 初始化
// 因为 parseInt() 不是编译期常量
// 字节码中仍然有对 Config.TIMEOUT 的引用坑二:枚举类的初始化
枚举类的 <clinit> 在第一次访问任何枚举常量时执行:
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 表达式不会触发其引用的类的初始化(通常只是加载):
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 寄存器(概述/面试题)。
