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. 框架中的类加载
- 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
)和设计高效的单例模式。