类加载初始化时机-主动引用与被动引用

4 阅读5分钟

在JVM类加载机制中,初始化阶段是执行类构造器 <clinit>() 方法的过程,只有当一个类被主动引用时,才会触发其初始化。相反,被动引用则不会导致类的初始化。理解这两者的区别,有助于掌握类加载的时机,避免不必要的初始化开销,并正确分析程序行为。


一、主动引用(Active Reference)

主动引用是指对类的使用方式满足以下五种情况之一,此时JVM必须立即对类进行初始化(如果尚未初始化)。这些情况由《Java虚拟机规范》严格定义:

  1. 遇到 newgetstaticputstaticinvokestatic 这四条字节码指令时,如果类没有初始化,则触发初始化。这对应代码中的场景:

    • 使用 new 关键字实例化对象。
    • 读取或设置一个类的静态字段(被 final 修饰且已在编译期放入常量池的静态字段除外)。
    • 调用一个类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有初始化,则触发初始化。

  3. 当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(但接口例外:初始化一个接口时,并不要求其父接口也完成初始化,除非真正用到父接口中定义的常量或方法)。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的类),虚拟机会先初始化这个主类

  5. 使用JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStaticREF_putStaticREF_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)

被动引用是指那些不会导致类初始化的引用方式。即使代码中出现了类的名字,但只要不满足上述五种情况,就不会触发初始化。常见的被动引用场景包括:

  1. 通过子类引用父类的静态字段(不会导致子类初始化)

    • 示例:System.out.println(Child.parentStaticVar);
    • 解释:parentStaticVar 是父类的静态字段,只有父类会被初始化,子类不会。
  2. 定义类的数组(不会触发类的初始化)

    • 示例:MyClass[] arr = new MyClass[10];
    • 解释:数组是JVM运行时动态生成的类型,其元素类型 MyClass 可能被加载,但不会被初始化(因为只是定义了数组,没有对类本身进行主动使用)。
  3. 引用类的编译时常量(常量已在编译期存入调用类的常量池,与类本身无关)

    • 示例:System.out.println(MyClass.CONSTANT);CONSTANTstatic final 的基本类型或String,值在编译时确定。
    • 解释:常量在编译期被内联到调用类的字节码中,对常量的引用不会触发定义常量的类初始化。
  4. 通过类名获取Class对象(不会触发初始化,除非反射调用导致)

    • 示例:Class<?> clazz = MyClass.class;
    • 解释:.class 字面量获取Class对象仅会触发类的加载,不会触发初始化。
  5. 使用 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 从未初始化。


三、主动引用与被动引用的对比

对比维度主动引用被动引用
触发初始化
典型场景newgetstaticputstaticinvokestatic、反射、父类初始化、主类、动态语言支持引用父类静态字段、定义数组、编译时常量、.class 获取、指定不初始化的 Class.forName
意义确保类在使用前已正确初始化延迟初始化,提高性能和灵活性
影响范围可能导致连锁初始化(父类先初始化)不会引发连锁初始化

四、为什么区分主动引用与被动引用?

  1. 性能优化:避免不必要的类初始化,缩短应用启动时间,减少内存占用(有些类可能永远用不到)。
  2. 正确性保证:确保类的静态资源在使用前已完成赋值和静态块逻辑。
  3. 安全性:防止通过某些方式提前执行类的静态代码块,造成安全隐患。

五、总结

  • 主动引用是类初始化的“导火索”,只有满足五种情况之一,JVM才会执行 <clinit>() 方法。
  • 被动引用允许我们在不初始化类的情况下使用类的部分信息(如常量、数组类型、Class对象),这体现了Java的延迟加载和按需初始化设计理念。
  • 在实际开发中,理解这两种引用有助于编写高效、正确的代码,尤其是在涉及复杂类依赖和静态初始化时。