类加载时机

5 阅读3分钟

JVM类加载时机由主动引用触发初始化(如实例化对象、访问静态变量或反射),而被动引用(如常量、数组定义等)不会触发,实现按需延迟加载以优化资源使用。


一、类加载的触发条件

类的加载(Loading)和初始化(Initialization)是两个不同的阶段,加载可以在程序运行中提前完成(如预加载),但初始化必须严格满足主动引用条件

1. 主动引用(触发初始化)

以下场景会触发类的 初始化阶段(即执行<clinit>()方法):

  • 场景 1:通过 new 实例化对象

    new MyClass();  // 触发MyClass的初始化
    
  • 场景 2:访问或修改类的静态变量(非常量)

    class MyClass {
        static int value = 1; // 非常量静态字段
    }
    int x = MyClass.value;    // 触发初始化
    MyClass.value = 2;        // 触发初始化
    
  • 场景 3:调用类的静态方法

    class MyClass {
        static void method() {}
    }
    MyClass.method();  // 触发初始化
    
  • 场景 4:反射调用(Class.forName()

    Class.forName("com.example.MyClass");  // 触发初始化(默认)
    // 若需仅加载不初始化,可指定initialize=false参数:
    Class.forName("com.example.MyClass", false, loader);
    
  • 场景 5:初始化子类时,若父类未初始化

    class Parent {}
    class Child extends Parent {}
    new Child();  // 先触发Parent初始化,再Child初始化
    
  • 场景 6:JVM 启动时标记的主类(包含 main() 方法的类)

    public class MainClass {
        public static void main(String[] args) {} // 主类会直接初始化
    }
    
  • 场景 7:动态语言支持(如 MethodHandle 解析后的调用) JDK 7+ 的 MethodHandle 在解析后的调用可能触发初始化。


2. 被动引用(不触发初始化)

以下场景 不会触发类的初始化

  • 场景 1:通过子类引用父类的静态字段

    class Parent {
        static int value = 1;
    }
    class Child extends Parent {}
    int x = Child.value;  // 仅触发Parent初始化,Child不初始化
    
  • 场景 2:定义类的数组

    MyClass[] arr = new MyClass[10];  // 不触发MyClass初始化
    
  • 场景 3:访问编译期常量(final static 字段)

    class MyClass {
        final static int CONST = 123;  // 常量在编译期直接替换到调用处
    }
    int x = MyClass.CONST;  // 不触发初始化
    
  • 场景 4:通过类名获取 Class 对象

    Class<MyClass> clazz = MyClass.class;  // 不触发初始化
    

二、接口的初始化

接口的初始化规则与类类似,但有以下区别:

  • 触发条件:首次使用接口的静态字段(即使字段是常量)。
  • 接口没有 <clinit>() 方法,除非接口中定义了静态字段赋值或静态代码块。

三、类加载的延迟性

JVM 的类加载是 按需延迟加载 的:

  1. 加载时机:类在首次被主动引用时才会加载。
  2. 验证阶段:部分验证可能在初始化阶段后才完成(如符号引用解析)。

四、实际应用中的触发场景

1. 框架中的类加载

  • Spring 的 Bean 加载:默认单例 Bean 在容器启动时初始化(触发类加载)。
  • 动态代理:通过 Proxy.newProxyInstance() 生成的代理类在首次调用时加载。

2. 延迟初始化(Lazy Initialization)

通过静态内部类实现单例模式的延迟加载:

class Singleton {
    private static class Holder {
        static final Singleton INSTANCE = new Singleton(); // 调用时触发初始化
    }
    public static Singleton getInstance() {
        return Holder.INSTANCE; // 首次调用时才加载Holder类
    }
}

3. JNI 本地方法

通过 JNI 调用本地方法(Native Method)时,不会触发类的初始化,除非本地方法中显式调用了 Java 方法。


五、观察类加载过程

可以通过 JVM 参数 -verbose:class-XX:+TraceClassLoading 打印类加载日志:

java -verbose:class MyApp

输出示例:

[Loaded com.example.MyClass from file:/path/to/classes/]
[Initialized com.example.MyClass]

六、总结

  • 主动引用:明确触发初始化(如 new、反射、静态字段访问)。
  • 被动引用:不触发初始化(如常量、数组定义、子类引用父类字段)。
  • 接口初始化:与类类似,但无 <clinit>() 方法。
  • 延迟加载:JVM 按需加载,避免资源浪费。

理解类加载时机有助于优化启动性能、解决类冲突(如 NoClassDefFoundError)和设计高效的单例模式。