JVM规范第 5 章:加载、链接与初始化
基于 Oracle 官方《The Java Virtual Machine Specification》第 5 章Java SE 26编写。这是什么为什么值得懂编译期产出的只是一堆字节。真正让new Foo()能工作、让父类静态变量先就绪、让NoClassDefFoundError这类异常有据可查的是第 5 章定义的生命周期三阶段加载Loading找到并读取类的二进制表示据此创建这个类/接口。链接Linking把类并入 JVM 运行时使其可被调用含验证、准备、解析。初始化Initialization执行类/接口的clinit方法真正跑你写的静态代码。这三个词常被混为一谈但它们在规范里是严格区分、顺序约束的三个阶段。读懂本章你能解释为什么父类一定先初始化为什么static final int在「准备」阶段有默认值、而显式赋值却在「初始化」阶段才发生ClassNotFoundException、NoClassDefFoundError、VerifyError、LinkageError各自在哪一阶段抛出那些「诡异的第一次访问才触发」行为懒加载从何而来。1. 总览三阶段与运行时结构JVM 是动态地加载、链接、初始化类和接口的——不是一次性把所有类都准备好而是按需发生。┌─────────────┐ 磁盘 │ class 文件 │ └──────┬──────┘ │ ① 加载 Loading ▼ ┌─────────────┐ │ 内存中创建类 │ 方法区构造内部表示运行时常量池建成 └──────┬──────┘ │ ② 链接 Linking │ ├─ 验证 Verification │ ├─ 准备 Preparation │ └─ 解析 Resolution (可延迟) ▼ ┌─────────────┐ │ ③ 初始化 │ 执行 clinit └─────────────┘1.1 运行时常量池每个类/接口都有运行时常量池由class文件里的constant_pool在创建时构造。池里两类条目符号引用symbolic references后续可能被「解析」§5.4.3成直接引用。如CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_MethodHandle/Type、CONSTANT_Dynamic、CONSTANT_InvokeDynamic。静态常量static constants无需进一步处理。CONSTANT_String_info经String.intern处理、数值常量按 IEEE 754。「可加载loadable」条目可被ldc系列指令压栈或作引导方法参数类/接口符号引用、方法句柄、方法类型、动态计算常量、静态常量。2. 加载2.1 核心概念定义加载器defining loader真正把类定义下来调用defineClass的加载器L。初始加载器initiating loader发起加载可能委托出去的加载器。一个类由二进制名 定义加载器唯一确定归属单一运行时包。类加载器分两类启动类加载器bootstrap与用户定义加载器ClassLoader子类。2.2 三种加载路径由类 D 的运行时常量池符号引用名 N 触发时N 是非数组类/接口且 D 由 bootstrap 定义→ 用 bootstrap 初始加载N 是非数组类/接口且 D 由用户加载器定义→ 同用户加载器初始加载N 是数组类→ JVM 直接创建数组类关联 D 的定义加载器。数组类的组件若是引用类型会递归加载组件类型数组类的定义加载器记为组件类的定义加载器否则记为 bootstrap。2.3 从 class 文件派生类这是最关键的「格式检查」步骤任何失败都会抛出对应Error权限/循环检查已在初始加载中 →LinkageError循环派生 →ClassCircularityError。解析二进制不是合法ClassFile→ClassFormatError版本不支持 →UnsupportedClassVersionError类名与实际不符 →NoClassDefFoundError。解析父类父类是接口或final→IncompatibleClassChangeError违反PermittedSubclasses密封类限制 → 同错。解析超接口超接口非接口 →IncompatibleClassChangeError同理检查许可子类。全部通过才标记 L 为定义加载器、记录初始加载器并在方法区创建内部表示。这也是为什么你之前看到的UnsupportedClassVersionError: major version 65实实在在发生在「加载」这一步——它根本还没到链接/运行。2.4 加载约束为保证类型安全当类CN1,L1引用DN2,L2的字段/方法时描述符中的类型名在 L1、L2 下必须是同一个类。约束形如NL1 NL2在准备/解析时强加。若两加载器都记录了初始加载 N、约束暗示相等、但实际加载出不同的类 → 撤销记录并抛LinkageError。2.5 模块与层类按运行时包归属某个运行时模块从而控制跨模块访问§5.4.4。defineModules创建模块并绑定类加载器「层layer」含 boot layer 与用户层。未命名模块unnamed module可读所有模块、导出所有包——这解释了为什么 classpath非模块路径上的代码几乎不受模块限制。3. 链接Linking链接可能抛OutOfMemoryError分配内存失败。它包含三步不必同时完成链接的硬性顺序约束完全加载后才链接完全验证、准备后才初始化链接错误在程序触发点抛出不是提前动态计算常量/invokedynamic调用点延迟解析。3.1 验证Verification确保二进制满足结构约束第 4 章 §4.9 的验证规则。不满足静态/结构约束 →VerifyError。因LinkageError失败的后续验证会重复同一错误。3.2 准备Preparation为静态字段分配空间并赋默认值第 2 章 §2.3/2.4不执行任何 Java 代码。例如publicstaticintx10;publicstaticbooleanflagtrue;在准备阶段x被设为0、flag被设为false——你写的 10/ true此时还没执行。同时强加加载约束重写方法的超类/超接口描述符类型需一致。准备必须在初始化之前完成。3.3 解析Resolution把符号引用替换为直接引用指针/偏移等。多条指令依赖解析getfield、invokevirtual、ldc等。解析类型与查找规则解析目标规则要点失败异常类/接口用定义加载器加载 N数组递归解析元素最后做访问控制NoClassDefFoundError等字段先解析所属类 C按 C → 超接口 → 父类查找字段NoSuchFieldError方法C 是接口 →IncompatibleClassChangeError查 C 及父类、超接口NoSuchMethodError接口方法C 非接口 →IncompatibleClassChangeError类似查找NoSuchMethodError方法类型/句柄方法类型解析其描述符中的类方法句柄按 9 种行为表 5.4.3.5-A解析见规范动态引导方法句柄 静态参数 → 调用 → 验证—字段/方法解析失败抛出的NoSuchFieldError/NoSuchMethodError都是LinkageError的子类——这类错误通常在首次主动使用时才暴露。3.4 访问控制Access Control解析后还要检查访问权限类 C 对 D 可访问同模块且 public、跨模块且被读且导出、或同包非 public字段/方法 R 对 D 可访问public、protected且 D 是子类、同包默认、或同 nest 的 private。失败 →IllegalAccessError同样属LinkageError家族。4. 初始化Initialization初始化 执行类/接口的clinit方法。它发生在加载、链接验证准备之后。显式静态字段赋值就是在这里跑的——这正是「准备阶段只给默认值」的补全。4.1 何时触发初始化6 类主动使用按规范类/接口在其首次主动使用时初始化。触发条件包括创建类的实例new调用类的静态方法使用或赋值类的静态字段非常量字段static final编译期常量除外见 §4.3调用类/接口的反射方法如Class.forName初始化其子类时父类必须先初始化父类优先JVM 启动时被指定为初始类含main方法的那个。注意static final基本类型/String 常量带ConstantValue属性在编译期就内联进使用处不触发定义类的初始化——这就是「常量不触发类加载」现象的根因。4.2clinit语义与死锁clinit由 JVM 保证线程安全多线程同时首次使用同一类只有一个线程执行clinit其余阻塞等待。但若clinit中又去主动使用另一个正在初始化的类可能造成死锁规范明确提示需警惕循环初始化依赖。接口初始化较宽松接口不要求其超接口先初始化除非用到超接口的非常量字段。初始化失败会抛ExceptionInInitializerError且该类后续再被使用会抛NoClassDefFoundError。5. 启动、绑定与终止启动JVM 通过 bootstrap 或用户加载器创建初始类链接、初始化它创建实例如需调用 main。初始类由实现相关方式指定通常命令行参数。绑定本地方法native方法在首次调用时把本地实现绑定到方法详见System.loadLibrary/ JNI 机制。终止JVM 在所有非守护线程结束后方可退出System.exit可主动终止。6. 一张「异常在哪个阶段抛出」速查表异常阶段含义ClassNotFoundException加载用户加载器找不到loadClass返回 null/未找到NoClassDefFoundError加载/解析链接时找不到类定义ClassFormatError加载派生不是合法ClassFileUnsupportedClassVersionError加载派生主版本不被支持ClassCircularityError加载派生类继承出现循环IncompatibleClassChangeError加载/解析父类是接口/final、密封类冲突等VerifyError链接验证字节码不满足约束NoSuchFieldError/NoSuchMethodError链接解析字段/方法不存在IllegalAccessError链接访问控制无访问权限LinkageError多阶段上述链接错误的父类ExceptionInInitializerError初始化clinit抛异常OutOfMemoryError链接分配内存不足7. 动手用-verbose:class观察加载顺序写两个有继承关系的类// Parent.javapublicclassParent{static{System.out.println(Parent clinit);}}// Child.javapublicclassChildextendsParent{static{System.out.println(Child clinit);}publicstaticvoidmain(String[]a){System.out.println(main runs);}}运行并打开类加载日志javac Parent.java Child.javajava-verbose:classChild你会看到先Parent后Child的clinit输出父类优先初始化以及 JVM 启动时大量 bootstrap 类的加载记录。再用Class.forName(Parent)对比「反射触发主动使用」体会懒加载的触发点差异。小结加载找字节、派生类、建内部表示失败抛ClassFormatError/UnsupportedClassVersionError等链接验证结构、准备静态字段默认值、解析符号→直接引用、访问控制初始化执行clinit首次主动使用时触发父类优先线程安全但警惕死锁大量LinkageError子类在对应阶段抛出是排错的关键坐标。