JVM类加载分加载、链接(验证/准备/解析)、初始化三阶段,Class.forName()默认触发初始化而new仅在未初始化时触发,static final编译时常量在准备阶段赋值,解析可延迟至首次使用前。

JVM 的类加载子系统不是“一次性把类读进来就完事”,而是分阶段、有条件、可延迟的协作机制——加载、链接(含验证/准备/解析)、初始化这三步,缺一不可,但顺序和时机常被误判。
什么时候真正触发类加载?Class.forName() 和 new 的行为差异
很多人以为只要写了 Class.forName("com.example.User") 就一定完成整个类加载流程,其实它只强制走到「初始化」;而 new User() 也会触发初始化,但前提是该类尚未被初始化过。关键区别在于:是否主动使用静态成员。
-
Class.forName(String)默认调用带initialize=true的重载,会触发初始化;若用Class.forName(name, false, loader),则跳过初始化,只到解析阶段 -
new实例化、访问static字段(非final常量)、调用static方法、反射获取Class对象并初始化——这些才是 JVM 规定的「主动使用」,会强制触发初始化 - 引用类型数组如
User[]的创建,不会触发User类加载;只有首次访问数组元素类型(比如arr[0] = new User())才加载User
static final 常量为什么在准备阶段就被赋值?
因为 JVM 把编译期能确定的字面量常量(即「编译时常量」)直接内联进调用处,不依赖运行时类的初始化。所以它的赋值发生在「准备阶段」,而非「初始化阶段」。
-
public static final int PORT = 8080;→ 准备阶段设为8080,后续所有引用都被编译器替换成字面量 -
public static final String NAME = "Tom";→ 同样适用,字符串字面量在常量池中已存在 -
public static final int CODE = compute();→ 不是编译时常量,compute()在初始化阶段才执行,准备阶段仍为默认值0
解析阶段为什么可以延迟?符号引用和直接引用到底差在哪?
解析的本质是把常量池里的符号引用(比如 CONSTANT_Methodref_info 中记录的 “类名+方法名+描述符”)转成内存中真实地址(如方法表指针、字段偏移量)。这个转换不必提前做,只要在第一次执行到相关字节码前完成即可。
- 接口方法调用(
invokeinterface)通常延迟解析,支持多态和动态代理场景 - 普通静态方法(
invokestatic)或私有方法(invokespecial)往往在初始化前就解析完毕 - 若解析失败(比如类没找到、方法签名不匹配),抛出
NoClassDefFoundError或IncompatibleClassChangeError,而不是ClassNotFoundException - 动态代理生成的类(如
$Proxy0)很多解析动作发生在Proxy.newProxyInstance()返回后、首次调用接口方法时
最容易被忽略的是:「加载」和「初始化」之间可能横跨数秒甚至更久——比如 Spring Boot 启动时大量类被加载进方法区,但直到某个 Controller 被首次请求,其 @PostConstruct 方法所在的类才真正初始化。这个时间差,正是热部署失败、静态变量未生效、或类加载器隔离异常的根源所在。










