JVM规范学习总结-JVM 的类生命周期JVM 如何加载、验证、链接和执行 Class 文件一、学习目标从 .class 文件追踪到机器指令二、第一阶段JVM 如何加载 Class 文件2.1 加载阶段的核心职责2.2 类加载器如何找到类2.3 HotSpot 中应当追踪哪些源码三、第二阶段JVM 如何验证 Class 文件3.1 为什么需要验证3.2 验证的主要检查内容3.3 一个简单的验证失败场景3.4 HotSpot 验证器的源码位置四、第三阶段JVM 如何链接 Class 文件4.1 准备创建静态字段并设置默认值4.2 解析将符号引用变成可用的运行时引用4.3 为什么解析不一定一次性完成五、第四阶段JVM 如何初始化 Class 文件对应的类5.1 类初始化做什么5.2 哪些操作会触发初始化5.3 类初始化的线程安全与失败状态5.4 HotSpot 的类状态六、第五阶段JVM 如何执行 Class 文件6.1 字节码执行时涉及哪些运行时结构6.2 解释器与 JIT 编译器解释执行JIT 编译七、用一份 Java 程序串起整个过程7.1 编译并检查生成的字节码7.2 观察 JVM 的类加载与初始化日志八、从系统工程师视角建立故障排查方法九、最终学习总结JVM 如何加载、验证、链接和执行 Class 文件一、学习目标从.class文件追踪到机器指令学习 JVM 的类生命周期不能只记住“加载、验证、准备、解析、初始化”几个名词而应当建立一条完整的技术链路Java 源码 → Class 文件 → 类加载器 → 字节码验证与链接 → 类初始化 → 字节码执行 → 解释器或 JIT 编译后的机器指令。根据《Java 虚拟机规范》JVMS第 5 章加载Loading、链接Linking和初始化Initialization是三个不同的概念。其中链接又包含验证Verification、准备Preparation和解析Resolution。JVM 可以根据实现选择提前或延迟进行部分工作但必须满足规范规定的约束。对系统工程师来说最重要的是区分三个层次规范层 JVMS 规定 Class 文件格式、字节码语义、验证约束和类初始化行为。虚拟机实现层 以 OpenJDK HotSpot 为例解析器、类元数据、解释器及 JIT 编译器负责实现这些行为。操作系统与硬件层 最终执行的机器指令、内存访问和调用约定受目标平台影响。JVMS 并不要求所有 JVM 都使用同一种解释器或 JIT 编译器因此 HotSpot 的具体实现不能直接等同于规范的全部要求。二、第一阶段JVM 如何加载 Class 文件2.1 加载阶段的核心职责加载Loading的目标是根据类的二进制名称找到对应的二进制表示并由 JVM 创建该类在运行时的内部表示。例如下面的 Java 源码packagedemo;publicclassPaymentService{publicintcalculate(intamount){returnamount*2;}}编译后生成demo/PaymentService.class。这个文件包含类的元数据、常量池、字段信息、方法信息和字节码等结构。JVM 加载这个类时主要完成以下工作找到 Class 文件的字节流来源可以是目录、JAR、网络或动态生成的数据。解析 Class 文件格式识别类名、父类、接口、字段、方法、属性等。创建 JVM 内部的类表示建立运行时常量池等运行时结构。确定该类的定义类加载器并建立相关类加载关系。JVMS 规定了类的创建与加载行为但不要求所有类都必须来自磁盘上的.class文件。例如数组类可以由 JVM 直接创建。2.2 类加载器如何找到类常见的类加载器包括类加载器主要职责Bootstrap Class Loader启动类加载器加载 JVM 启动所需的核心类等Platform Class Loader平台类加载器加载平台相关的类库Application/System Class Loader应用类加载器加载应用类路径、模块路径中的应用类自定义 ClassLoader从指定的目录、网络、加密文件或自定义存储中加载类在 OpenJDK 的默认ClassLoader实现中loadClass的主要逻辑是// 对应 OpenJDK// src/java.base/share/classes/java/lang/ClassLoader.java//// 下面是帮助理解的简化逻辑不是完整源码。protectedClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{// 1. 先查询当前加载器是否已经加载过该类。Class?cfindLoadedClass(name);if(cnull){try{// 2. 默认优先委派给父加载器。if(getParent()!null){cgetParent().loadClass(name,false);}else{// 3. 没有父加载器时尝试由 JVM 内置的// 启动类加载器加载。cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptionignored){// 父加载器找不到时继续尝试当前加载器。}if(cnull){// 4. 由当前加载器查找并定义类。cfindClass(name);}}// 5. resolve 为 true 时请求执行解析。// 这不代表初始化该类也不代表必须提前解析// 这个类的所有符号引用。if(resolve){resolveClass(c);}returnc;}源码参考OpenJDK 的 ClassLoader.java尤其是loadClass和findClass方法。上述代码是用于说明控制流程的简化版本。这里有一个系统工程师必须掌握的边界双亲委派是默认类加载器常用的实现策略并不是所有自定义类加载器都必须采用的唯一策略。另外运行时类的身份不能只由类名确定还与定义它的类加载器有关。两个不同的定义类加载器即使加载相同二进制名称的类也可能产生不同的运行时类型。这对插件隔离、应用服务器和模块化系统尤其重要。2.3 HotSpot 中应当追踪哪些源码OpenJDK 源码入口学习重点src/java.base/share/classes/java/lang/ClassLoader.javaJava 层的加载委派、查找和类定义入口src/hotspot/share/classfile/systemDictionary.cppJVM 内部的类查找、解析、并发加载协调src/hotspot/share/classfile/classFileParser.cpp解析 Class 文件并构建内部类元数据SystemDictionary::resolve_instance_class_or_null是 HotSpot 处理类名解析的重要入口之一ClassFileParser则负责 Class 文件的解析工作。它们分别体现了类加载的协调层和文件解析层。学习总结 1 类加载并不等于执行类中的 Java 代码。它首先解决的是“这个运行时类型是什么、从哪里来、由谁定义”。三、第二阶段JVM 如何验证 Class 文件3.1 为什么需要验证Class 文件是 JVM 的输入。它可能由javac生成也可能由字节码增强工具、动态代理、其他编译器或自定义类加载器生成。因此JVM 不能简单地假设每个 Class 文件都可信、格式正确、字节码合法。验证Verification用于检查类或接口的二进制表示是否符合 Class 文件结构和 JVM 类型安全规则。JVMS 第 5.4.1 节描述这一阶段第 4.9 和第 4.10 节则规定结构约束与验证规则。3.2 验证的主要检查内容检查层次主要检查内容系统工程视角文件格式魔数、版本、常量池与属性结构是否合法防止非法二进制输入元数据结构类、字段、方法、访问标志是否符合格式约束确认类的结构可以被正确解释类型与继承关系父类、接口、方法签名等是否满足规范避免运行时类型关系不一致字节码静态约束指令合法性、跳转位置、操作数栈使用等避免无效指令和非法控制流字节码类型安全局部变量、操作数栈上的值是否符合类型规则避免把不兼容类型当作合法引用使用例如Class 文件一般以0xCAFEBABE作为魔数开头。但仅检查这个魔数是不够的即使文件开头正确方法字节码仍可能不满足 JVM 的类型规则。对于方法中的字节码验证器会检查控制流路径、操作数栈和局部变量的类型状态等。现代 Class 文件通常还包含StackMapTable等验证相关属性帮助验证器检查控制流中的类型状态。3.3 一个简单的验证失败场景假设某个字节码生成工具错误地修改了一个方法使操作数栈类型不合法或者违反某条 JVM 指令的类型约束。即使文件本身能够被读取JVM 仍可能拒绝该类。常见相关错误包括ClassFormatErrorClass 文件格式非法。UnsupportedClassVersionError当前 JVM 不支持该 Class 文件版本。VerifyError验证过程中发现不符合 JVM 规则的字节码。其他LinkageError子类可能来自链接过程中的不同失败条件。注意错误的出现时间并不固定在加载刚结束的那个瞬间。某些链接检查可以延迟到程序执行了真正需要相关类型或符号引用的操作时才暴露。3.4 HotSpot 验证器的源码位置学习时可以继续阅读src/hotspot/share/classfile/verifier.cpp这是 HotSpot 字节码验证实现的重要源码入口。可将它与classFileParser.cpp对照阅读ClassFileParser负责理解 Class 文件结构。Verifier负责检查类及其字节码是否满足相关约束。学习总结 2 验证关注的是“这个 Class 文件能否被 JVM 按规范安全地理解和执行”而不是检查业务逻辑是否正确。例如验证通过并不表示程序不会出现空指针异常、死循环或业务错误。四、第三阶段JVM 如何链接 Class 文件在 JVMS 中链接Linking包括验证、准备和解析。这里需要注意验证是链接的一部分不能把“链接”简单理解成验证之后的某一步。4.1 准备创建静态字段并设置默认值准备阶段会创建类或接口的静态字段并将其初始化为对应类型的默认值。显式静态字段初始化表达式则属于后续的初始化阶段而不是准备阶段。例如publicclassConfig{// 准备阶段对应的默认值为 0。staticinttimeout;// 准备阶段对应的默认值为 null// 赋值 new Object() 属于初始化逻辑。staticObjectclientnewObject();// 编译期常量变量// 编译后的 Class 文件可能为它保存 ConstantValue 属性。staticfinalintMAX_RETRY3;static{// 这段逻辑属于类初始化而非准备阶段。timeout5000;}}从运行时角度可以把它理解为字段准备阶段的默认值类初始化时的行为timeout0执行静态块将其设为5000clientnull执行new Object()并赋值MAX_RETRY建立相应静态字段按规范处理ConstantValue属性所指定的值JVMS 对ConstantValue属性还有专门规定类初始化过程中会处理相关常量值然后执行类初始化方法。因此不要把“准备时设置默认值”和“类初始化时进行赋值”混为一谈。4.2 解析将符号引用变成可用的运行时引用Class 文件常量池中包含大量符号引用例如某个类或接口的名称某个字段的名称和描述符某个方法的名称和描述符方法句柄、方法类型以及动态计算常量等相关信息。例如publicclassOrderService{publicintprocess(){// 编译后会涉及对其他类、方法或字段的引用。// Class 文件通常以常量池符号引用记录这些依赖。returnPriceCalculator.calculate(100);}}编译器可以在OrderService.class的常量池里记录与PriceCalculator.calculate有关的符号信息。运行时JVM 需要根据这些符号信息确定实际类、方法及其访问合法性。这就是解析Resolution的核心职责。解析可能检查目标类或接口是否存在。目标字段或方法是否存在。字段或方法的描述符是否匹配。当前类是否有权访问目标成员。相关类加载约束是否满足。解析失败可能出现NoSuchFieldError、NoSuchMethodError、IllegalAccessError或IncompatibleClassChangeError等错误。4.3 为什么解析不一定一次性完成JVMS 允许实现灵活选择解析时机。例如提前解析Eager Resolution 在较早阶段处理符号引用。延迟解析Lazy Resolution 等到某个符号引用真正被使用时再处理。规范要求一个类先完成加载再进行链接类在初始化之前必须已经完成验证和准备但不要求所有符号引用必须在同一时刻全部解析完成。这种设计对系统性能很重要如果某个分支从未运行JVM 不一定需要立即解析该分支涉及的所有依赖。但这也意味着有些类缺失或方法不匹配的问题可能直到某条路径实际执行时才暴露。学习总结 3 准备解决静态存储的初始状态解析解决符号依赖的绑定问题。它们解决的是不同层次的问题不能互相替代。五、第四阶段JVM 如何初始化 Class 文件对应的类5.1 类初始化做什么初始化Initialization是在 JVM 中执行类初始化逻辑的阶段核心是执行编译器生成的类初始化方法clinit如果该类需要该方法。例如publicclassStartupConfig{staticintportreadPort();static{// 这些语句会纳入类初始化逻辑。System.out.println(StartupConfig initialized);}staticintreadPort(){return8080;}}在通常的javac编译过程中静态字段初始化表达式和静态代码块会按照源码中的顺序组合到clinit方法中。实例构造方法init则是另外一回事它负责对象实例的构造不是类初始化方法。5.2 哪些操作会触发初始化按照 JVMS 的类初始化规则常见触发场景包括执行new指令创建某个类的对象。读取或写入某个类声明的静态字段对应getstatic或putstatic。调用某个类声明的静态方法对应invokestatic。首次调用符合规范规定条件的某些静态方法句柄。应用程序启动时JVM 按启动规则初始化入口类。需要特别留意对于getstatic、putstatic和invokestatic规范关注的是实际声明相关字段或方法的类或接口而不只是源码里写出的表面引用类型。并不是每一次“看到一个类名”都会触发初始化。例如调用SomeClass.class获得类字面量或通过某些不要求初始化的加载操作取得Class对象并不等价于执行其静态初始化逻辑。5.3 类初始化的线程安全与失败状态JVM 必须处理多个线程同时请求初始化同一个类的情况并且要支持递归初始化请求。规范要求实现维护足以区分以下状态的信息已经验证和准备但尚未初始化正在由某个线程初始化已完成初始化初始化失败类处于错误状态。这意味着类初始化具有同步语义。其他线程若遇到同一个类正在由另一个线程初始化可能需要等待结果。假如初始化异常终止该类会进入错误状态后续初始化请求不能简单地重新执行初始化方法。5.4 HotSpot 的类状态在 OpenJDK HotSpot 中src/hotspot/share/oops/instanceKlass.hpp定义了与类加载、链接和初始化有关的状态例如// 对应 OpenJDK HotSpot 的 InstanceKlass 类状态。// 这里展示与学习主题相关的状态名称及注释。enumClassState:u1{allocated,// 已分配内部表示尚未完成加载loaded,// 已加载但尚未链接完成linked,// 已链接成功但尚未初始化being_initialized,// 正在执行类初始化fully_initialized,// 初始化成功完成initialization_error// 初始化发生错误};源码参考OpenJDK HotSpot 的 instanceKlass.hpp。源码里的枚举有助于建立实现层面的认识但要记住这些具体状态名称和内部存储方式属于 HotSpot 实现不是 JVMS 要求所有 JVM 必须采用的内部数据结构。学习总结 4 类初始化不是一个无条件立即发生的阶段。它由规范规定的使用场景触发必须确保同一类的初始化安全协调并正确处理初始化失败。六、第五阶段JVM 如何执行 Class 文件类加载完成不意味着程序就已经执行了它的所有方法。只有在运行时需要调用某个方法时JVM 才会依照该方法的调用语义执行对应操作。6.1 字节码执行时涉及哪些运行时结构理解执行过程可以先抓住以下几个核心概念运行时结构执行时的主要作用运行时常量池提供常量、符号引用等信息Java 虚拟机栈为方法调用提供栈帧栈帧中的局部变量表保存方法参数和局部变量栈帧中的操作数栈为字节码指令计算和传递操作数程序计数器等执行状态支持方法执行位置和执行过程管理堆及类元数据相关结构存放对象和运行时类型信息等当 JVM 执行方法时会创建或使用相应栈帧按字节码语义读取局部变量、对操作数栈进行计算、访问字段、调用其他方法并处理返回值或异常。具体实现可以根据 JVM 的实现策略进行优化。6.2 解释器与 JIT 编译器在 HotSpot 这类 JVM 中执行通常既涉及解释器也可能涉及即时编译器JIT。解释执行解释器依据字节码语义逐步执行并维护运行时状态。JIT 编译热点方法或代码路径可以编译为目标处理器的机器码。目标平台上的机器指令具体指令、寄存器、内存访问和调用约定取决于目标平台及 JVM 实现JVMS 规定虚拟机指令的语义但不要求必须以传统解释器逐条执行每一条字节码。HotSpot 的TemplateInterpreter是解释器实现的重要入口而编译策略相关代码则负责参与方法的编译决策。在系统层面应当重点理解字节码是 JVM 的指令集不是目标处理器的机器码。JIT 可以进行内联、常量传播、消除冗余操作等优化。编译后的代码仍然必须满足 Java 程序的可观察语义。在支持反优化的执行引擎中如果优化假设不再成立JVM 可能从编译代码退回解释执行或其他适当的执行状态。学习总结 5 Class 文件描述的是与平台相对独立的 JVM 执行语义解释器和 JIT 则是实现这些语义的不同执行手段。七、用一份 Java 程序串起整个过程下面的示例可以作为学习实验。重点关注类加载、初始化和方法执行的区别。publicclassLifecycleDemo{// 准备阶段设置为默认值 0。// 类初始化期间再执行源码中的显式赋值。privatestaticintconfiguredValueinitializeValue();// 编译期常量变量。编译器可能将它的使用处直接内联。privatestaticfinalintCONSTANT42;// 静态代码块的输出会进入类初始化逻辑。static{System.out.println([init] LifecycleDemo initialized);}privatefinalintinstanceValue;// 实例构造逻辑对应 init不是 clinit。publicLifecycleDemo(intvalue){this.instanceValuevalue;System.out.println([instance] constructor executed);}privatestaticintinitializeValue(){System.out.println([init] initializeValue executed);return100;}publicstaticintadd(inta,intb){// 该方法编译后生成字节码// 运行时可被解释执行也可被 JIT 编译。returnab;}publicstaticvoidmain(String[]args){// 启动流程已经完成必要的入口类加载、链接和初始化// 这里开始执行 main 方法体。System.out.println([main] configuredValue configuredValue);System.out.println([main] CONSTANT CONSTANT);// new 会触发目标类的初始化如果尚未初始化// 然后创建实例并执行构造方法。LifecycleDemodemonewLifecycleDemo(7);// 这是普通方法调用。intresultadd(demo.instanceValue,5);System.out.println([main] result result);}}7.1 编译并检查生成的字节码# 编译源码生成 LifecycleDemo.classjavac-gLifecycleDemo.java# 查看 Class 文件的详细结构、常量池和方法字节码javap-v-c-pLifecycleDemo重点观察Constant pool类名、方法名、字段引用、字符串常量等。descriptor方法参数与返回类型的描述符。Code属性方法的字节码、最大操作数栈深度及局部变量表等信息。init实例构造逻辑。clinit静态字段初始化表达式和静态代码块组合后的类初始化逻辑若需要生成。7.2 观察 JVM 的类加载与初始化日志在支持统一日志的现代 HotSpot 中可以运行java-Xlog:classloadinfo,classinitinfo LifecycleDemo这条命令用于观察类加载和初始化相关日志。日志中还会涉及 Java 核心类等大量其他类型因此不应期待只输出LifecycleDemo。在排查问题时可以按具体问题选择日志类别和级别并结合javap的字节码输出与对应的 OpenJDK 源码进行验证而不只是根据程序最终打印的顺序反推 JVM 的所有内部步骤。八、从系统工程师视角建立故障排查方法将类生命周期与常见故障对应起来比单独记忆每个阶段更有价值。现象或错误优先调查方向需要确认的事实ClassNotFoundException类加载器的查找路径指定加载器是否能够找到目标类NoClassDefFoundError加载或链接依赖、初始化历史依赖类是否缺失目标类是否曾经初始化失败UnsupportedClassVersionError编译和运行时版本当前 JVM 是否支持该 Class 文件版本VerifyErrorClass 文件与字节码验证字节码增强、编译器输出或类文件是否违反验证规则NoSuchMethodError/NoSuchFieldError二进制依赖兼容性编译时依赖和运行时实际加载的版本是否不一致ExceptionInInitializerError类初始化执行静态字段初始化或静态代码块是否抛出了异常类加载耗时高类加载和链接路径类的数量、加载来源、重复加载和解析成本程序前期执行慢、后续变快解释执行及 JIT 行为是否发生编译、代码优化或反优化这里还有一个实践原则不能看到“类相关错误”就一律归因于类加载器。 例如NoSuchMethodError往往说明运行时实际类的二进制结构与调用方的预期不一致ExceptionInInitializerError则应当沿初始化方法及其调用链排查。九、最终学习总结从系统工程师角度看JVMS 中 Class 文件的生命周期可以归纳为六个关键问题阶段核心问题加载这个运行时类型从哪里来由谁定义验证Class 文件及其字节码是否符合 JVM 规范准备类的静态字段与运行时结构是否完成初始准备解析常量池中的符号引用如何绑定到实际类型、字段和方法初始化何时执行静态初始化逻辑如何确保线程安全执行字节码如何由解释器或编译器实现其语义进一步阅读时建议依次研读以下资料JVMS 第 5 章Loading, Linking, and Initializing —— 掌握规范要求。OpenJDKClassLoader.java—— 理解 Java 层的类加载流程。OpenJDKclassFileParser.cpp与systemDictionary.cpp—— 理解 HotSpot 的类解析与加载协调。OpenJDKinstanceKlass.hpp—— 理解类状态及初始化相关结构。OpenJDKtemplateInterpreter.cpp—— 进入解释器执行路径。最终要建立的认识是JVM 的核心任务不是简单地读取.class文件而是把符合规范的二进制类型表示转化为可以安全链接、正确初始化并最终执行的运行时实体。 加载、验证、链接和执行是同一条运行链路上的不同职责掌握它们之间的边界才能把规范阅读、HotSpot 源码分析、故障排查与性能分析真正连接起来。