类加载必须分加载、链接、初始化三阶段,根本原因在于jvm需严格分离字节码载入、结构校验与内存布局、java层逻辑执行三类操作;混做会导致校验失效、初始化提前或破坏双亲委派安全边界。

加载、链接、初始化三阶段不是设计选择,而是语义隔离的必然结果
类加载必须分三阶段,根本原因在于 JVM 需要严格分离「字节码载入」「结构校验与内存布局」「Java 层逻辑执行」这三类不同性质的操作。混在一起做,要么校验失效,要么初始化提前触发,要么破坏双亲委派的安全边界。
加载阶段只做三件事:读字节流、建运行时结构、生成 Class 对象
这个阶段完全不执行任何 Java 代码,也不解析符号引用,更不碰静态变量赋值。它只负责把 .class 文件变成方法区里的 InstanceKlass 结构体,并在堆里配一个对应的 Class 对象——后者只是个“门面”,和 InstanceKlass 通过双向指针关联。
-
ClassLoader.defineClass()返回后,Class对象已存在,但类还没初始化,static {}块没跑,static final int x = 123这种编译期常量虽已就位,但普通static int y = 456还是默认值 0 - 如果此时用反射调用
getDeclaredFields(),能看到字段定义,但读y会得到 0;写y是允许的,但不会触发初始化 - 这个阶段失败只会抛
ClassFormatError或NoClassDefFoundError(找不到字节流),不会出现ExceptionInInitializerError
链接阶段拆成验证、准备、解析,每步都卡在不可绕过的检查点
验证(Verification)不是可选优化,而是字节码安全的底线。JVM 必须确保指令流不会越界跳转、栈帧不会类型错配、符号引用能被后续解析——这些都得在初始化前确认,否则初始化中途崩溃,状态无法回滚。
- 准备(Preparation)只为静态变量分配内存并设默认值(如
int→0,Object→null),但跳过final static字段,因为它们的值已在编译期固化进常量池,由ldc指令直接加载 - 解析(Resolution)延迟到首次主动使用符号引用时才发生(比如第一次调用某个方法),所以它被标记为“非必须”——某些类可能永远不触发解析,比如只用到其
Class对象但没调用任何成员 - 如果解析失败(如
java.lang.NoSuchMethodError),说明常量池里写的签名和目标类实际签名不一致,问题往往出在 javac 泛型擦除时机或字节码增强工具改错了 descriptor
初始化只响应五种明确指令,且保证单次、线程安全
初始化不是“类加载完就自动跑”,而是被 new、getstatic、putstatic、invokestatic、invokedynamic(JDK7+)这五类字节码显式触发。JVM 用一个 initialization_state 标志位 + CAS 控制,确保即使多线程并发触发,也只执行一次 <clinit></clinit> 方法。
- 子类初始化时,父类一定已初始化完成,但父类的
<clinit></clinit>不会重复执行——这是双亲委派在运行时的延续,不是靠类加载器传递,而是靠 JVM 内部状态同步 - 接口有初始化,但规则不同:只有当接口中定义了 default 方法,且该 default 方法被首次调用时,接口才初始化;纯抽象接口即使被实现类加载,也不会触发自身初始化
- 最容易踩的坑是:在
<clinit></clinit>里调用尚未初始化的其他类的静态方法,造成死锁(比如 A.调 B.staticMethod,B. 又等 A 初始化完成)
SystemDictionary::resolve_class() 控制加载入口,LinkResolver::resolve_method() 处理解析,InstanceKlass::initialize() 封装初始化逻辑——改其中任一环节,都可能让 VerifyError 变成 Segmentation Fault。











