在JVM类加载机制中,初始化阶段是执行类构造器 <clinit>() 方法的过程,只有当一个类被主动引用时,才会触发其初始化。相反,被动引用则不会导致类的初始化。理解这两者的区别,有助于掌握类加载的时机,避免不必要的初始化开销,并正确分析程序行为。
一、主动引用(Active Reference)
主动引用是指对类的使用方式满足以下五种情况之一,此时JVM必须立即对类进行初始化(如果尚未初始化)。这些情况由《Java虚拟机规范》严格定义:
-
遇到
new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有初始化,则触发初始化。这对应代码中的场景:- 使用
new关键字实例化对象。 - 读取或设置一个类的静态字段(被
final修饰且已在编译期放入常量池的静态字段除外)。 - 调用一个类的静态方法。
- 使用
-
使用
java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化,则触发初始化。 -
当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(但接口例外:初始化一个接口时,并不要求其父接口也完成初始化,除非真正用到父接口中定义的常量或方法)。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()方法的类),虚拟机会先初始化这个主类。 -
使用JDK 1.7 的动态语言支持时,如果一个
java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
示例:主动引用触发初始化
public class ActiveRef {
public static void main(String[] args) throws Exception {
// 1. new 对象
MyClass obj = new MyClass(); // 触发 MyClass 初始化
// 2. 访问静态字段
System.out.println(MyClass.staticVar); // 触发初始化(如果之前未初始化)
// 3. 调用静态方法
MyClass.staticMethod(); // 触发初始化
// 4. 反射调用
Class<?> clazz = Class.forName("MyClass"); // 默认会初始化
// 如果使用 Class.forName("MyClass", false, loader) 则不会初始化,但后续反射访问字段/方法会触发
// 5. 初始化子类时,父类也会先初始化(见后续示例)
}
}
class MyClass {
public static int staticVar = 10;
static {
System.out.println("MyClass 初始化");
}
public static void staticMethod() {}
}
注意:对于第5种主动引用(动态语言支持),日常开发较少遇到,暂不展开。
二、被动引用(Passive Reference)
被动引用是指那些不会导致类初始化的引用方式。即使代码中出现了类的名字,但只要不满足上述五种情况,就不会触发初始化。常见的被动引用场景包括:
-
通过子类引用父类的静态字段(不会导致子类初始化)
- 示例:
System.out.println(Child.parentStaticVar); - 解释:
parentStaticVar是父类的静态字段,只有父类会被初始化,子类不会。
- 示例:
-
定义类的数组(不会触发类的初始化)
- 示例:
MyClass[] arr = new MyClass[10]; - 解释:数组是JVM运行时动态生成的类型,其元素类型
MyClass可能被加载,但不会被初始化(因为只是定义了数组,没有对类本身进行主动使用)。
- 示例:
-
引用类的编译时常量(常量已在编译期存入调用类的常量池,与类本身无关)
- 示例:
System.out.println(MyClass.CONSTANT);且CONSTANT是static final的基本类型或String,值在编译时确定。 - 解释:常量在编译期被内联到调用类的字节码中,对常量的引用不会触发定义常量的类初始化。
- 示例:
-
通过类名获取Class对象(不会触发初始化,除非反射调用导致)
- 示例:
Class<?> clazz = MyClass.class; - 解释:
.class字面量获取Class对象仅会触发类的加载,不会触发初始化。
- 示例:
-
使用
Class.forName()指定initialize=false- 示例:
Class.forName("MyClass", false, classLoader); - 解释:该方法允许获取Class对象而不触发初始化。
- 示例:
示例:被动引用不会触发初始化
public class PassiveRef {
public static void main(String[] args) {
// 1. 通过子类引用父类的静态字段
System.out.println(SubClass.parentStaticVar); // 只初始化 Super,不初始化 Sub
// 2. 定义数组
SubClass[] arr = new SubClass[10]; // 不初始化 SubClass
// 3. 引用编译时常量
System.out.println(SubClass.CONST); // 不初始化 SubClass(CONST 是编译时常量)
// 4. 使用 .class 获取Class对象
Class<?> clazz = SubClass.class; // 不初始化 SubClass
}
}
class Super {
public static int parentStaticVar = 1;
static {
System.out.println("Super 初始化");
}
}
class SubClass extends Super {
public static final int CONST = 100; // 编译时常量
static {
System.out.println("SubClass 初始化");
}
}
运行 PassiveRef 的输出:
Super 初始化
1
可以看到只有父类 Super 被初始化,子类 SubClass 从未初始化。
三、主动引用与被动引用的对比
| 对比维度 | 主动引用 | 被动引用 |
|---|---|---|
| 触发初始化 | 是 | 否 |
| 典型场景 | new、getstatic、putstatic、invokestatic、反射、父类初始化、主类、动态语言支持 | 引用父类静态字段、定义数组、编译时常量、.class 获取、指定不初始化的 Class.forName |
| 意义 | 确保类在使用前已正确初始化 | 延迟初始化,提高性能和灵活性 |
| 影响范围 | 可能导致连锁初始化(父类先初始化) | 不会引发连锁初始化 |
四、为什么区分主动引用与被动引用?
- 性能优化:避免不必要的类初始化,缩短应用启动时间,减少内存占用(有些类可能永远用不到)。
- 正确性保证:确保类的静态资源在使用前已完成赋值和静态块逻辑。
- 安全性:防止通过某些方式提前执行类的静态代码块,造成安全隐患。
五、总结
- 主动引用是类初始化的“导火索”,只有满足五种情况之一,JVM才会执行
<clinit>()方法。 - 被动引用允许我们在不初始化类的情况下使用类的部分信息(如常量、数组类型、Class对象),这体现了Java的延迟加载和按需初始化设计理念。
- 在实际开发中,理解这两种引用有助于编写高效、正确的代码,尤其是在涉及复杂类依赖和静态初始化时。