JVM规范学习总结:解释器与JIT编译器如何处理JVM字节码

JVM规范学习总结:解释器与JIT编译器如何处理JVM字节码

  • 前言
  • [解释器与 JIT 编译器如何处理 JVM 字节码](#解释器与 JIT 编译器如何处理 JVM 字节码)
    • [一、学习目标:理解字节码如何转化为 CPU 执行的指令](#一、学习目标:理解字节码如何转化为 CPU 执行的指令)
    • 二、先建立完整的执行路径
    • [三、JVMS 如何定义字节码的执行](#三、JVMS 如何定义字节码的执行)
      • [1. 字节码不是直接对应 CPU 指令](#1. 字节码不是直接对应 CPU 指令)
      • [2. 使用一个示例理解字节码](#2. 使用一个示例理解字节码)
    • [四、解释器如何处理 JVM 字节码](#四、解释器如何处理 JVM 字节码)
      • [1. 解释器的核心工作流程](#1. 解释器的核心工作流程)
      • [2. HotSpot 的模板解释器](#2. HotSpot 的模板解释器)
      • [3. 解释器为什么通常效率较低](#3. 解释器为什么通常效率较低)
    • [五、JIT 编译器如何处理字节码](#五、JIT 编译器如何处理字节码)
      • [1. JIT 的目标:从字节码生成优化机器码](#1. JIT 的目标:从字节码生成优化机器码)
      • [2. HotSpot 的 C1 与 C2](#2. HotSpot 的 C1 与 C2)
      • [3. JIT 如何判断一个方法是热点](#3. JIT 如何判断一个方法是热点)
      • [4. JIT 如何将字节码转化为中间表示](#4. JIT 如何将字节码转化为中间表示)
    • [六、JIT 的关键优化机制](#六、JIT 的关键优化机制)
      • [1. 方法内联(Inlining)](#1. 方法内联(Inlining))
      • [2. 基于类型剖析的优化](#2. 基于类型剖析的优化)
      • [3. 其他常见优化](#3. 其他常见优化)
    • [七、JIT 优化后,JVM 如何保证程序语义正确](#七、JIT 优化后,JVM 如何保证程序语义正确)
      • [1. 去优化(Deoptimization)](#1. 去优化(Deoptimization))
      • [2. 栈上替换(OSR)](#2. 栈上替换(OSR))
    • 八、从系统工程师角度验证上述结论
      • [1. 查看字节码](#1. 查看字节码)
      • [2. 观察编译事件](#2. 观察编译事件)
      • [3. 比较解释执行与编译执行](#3. 比较解释执行与编译执行)
    • 九、最终学习总结
  • [十、解释器与 JIT 编译器的源码对照总结](#十、解释器与 JIT 编译器的源码对照总结)
    • [1. 规范层与实现层的区别](#1. 规范层与实现层的区别)
    • [2. 推荐的源码阅读顺序](#2. 推荐的源码阅读顺序)
    • [3. 最小实验:验证字节码与编译行为](#3. 最小实验:验证字节码与编译行为)
    • 十一、最终结论

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

解释器与 JIT 编译器如何处理 JVM 字节码

一、学习目标:理解字节码如何转化为 CPU 执行的指令

从系统工程师的角度研究 JVM,不能只停留在"Java 代码经过编译后生成字节码,最后由 JVM 执行"这一层面,还需要进一步回答以下问题:

  • JVM 解释器读取字节码后,如何完成取指、解码、操作数处理和控制流跳转?
  • 为什么解释器执行相同的字节码时,通常会比经过 JIT 优化的机器码更慢?
  • JVM 如何判断一个方法或循环值得编译?C1 和 C2 编译器分别负责什么?
  • JIT 编译器进行了方法内联、常量传播等优化之后,如何保证程序仍满足 JVM 规范?
  • 当 JIT 编译时采用的假设失效,JVM 如何从机器码执行状态回到解释器?

本次学习以 Java SE 26 的 《Java 虚拟机规范》(JVMS) 为规范依据,并结合 OpenJDK HotSpot 源码分析具体实现。需要先明确:JVMS 规定字节码的语义和 JVM 必须满足的行为,但不强制规定 JVM 必须使用某一种解释器、JIT 编译器或优化算法。 字节码翻译成宿主 CPU 的机器码,是规范允许的实现方式之一。

二、先建立完整的执行路径

需要注意,这张图表示的是典型的 HotSpot 执行路径,而不是说每个方法都必定经历完整的解释器、C1、C2 流程。不同方法可能始终由解释器执行,也可能只经过部分编译层级;具体行为取决于 JVM 配置、编译策略、方法特征及运行时反馈。

三、JVMS 如何定义字节码的执行

1. 字节码不是直接对应 CPU 指令

Java 编译器 javac 生成的是 JVM 指令,例如:

复制代码
iload_0
bipush
imul
iadd
istore_2
if_icmple
ireturn

这些是 JVM 指令,不是 x86-64 的 IMUL、ADD 或 AArch64 的 MUL、ADD 等宿主 CPU 指令。

JVMS 第 2 章说明了 JVM 的抽象机器、运行时数据区、栈帧和指令集;第 6 章定义各条指令的具体语义。JVM 实现可以采用不同的内部方式执行它们,但必须满足相应的规范要求。参见 JVMS 第 2 章 与 JVMS 第 6 章。

2. 使用一个示例理解字节码

下面以一个简单方法作为分析对象:

java 复制代码
public final class BytecodeDemo {

    // x、y 是参数;product、result 是局部变量。
    static int calc(int x, int y) {
        int product = x * 31;
        int result = product + y;

        if (result > 100) {
            return result;
        }

        return result + 1;
    }
}

可以使用以下命令查看编译结果:

bash 复制代码
javac BytecodeDemo.java
javap -c -v BytecodeDemo

在典型的 javac 输出中,calc 方法的字节码大致如下;具体偏移和指令排列应以实际 JDK 版本的 javap 输出为准。

复制代码
iload_0       // 读取局部变量 x,将值压入操作数栈
bipush 31     // 将常量 31 压入操作数栈
imul          // 弹出两个 int,计算乘积,再将结果压栈
iload_1       // 读取局部变量 y
iadd          // 弹出两个 int,相加并将结果压栈
istore_2      // 将计算结果保存到局部变量 product/result 对应槽位

iload_2       // 加载待判断的整数
bipush 100    // 压入比较常量
if_icmple     // 比较两个整数,满足条件则跳转

iload_2       // 重新加载返回值
ireturn       // 返回 int 结果

// 条件跳转的另一条执行路径
iload_2       // 加载原结果
iconst_1      // 压入整数 1
iadd          // 计算 result + 1
ireturn       // 返回 int 结果

以上注释展示的是指令的职责,不代表 JVM 一定将每条指令逐条转换成一条 CPU 机器指令。

学习时需要特别注意:

  • 局部变量表:保存方法参数及部分局部变量。
  • 操作数栈:字节码运算通常从栈中取出操作数,再将结果压回栈中。
  • 程序计数器及当前执行位置:用于确定下一条需要执行的指令;具体实现不必采用规范所描述概念的字面内存布局。
  • 控制流:条件分支、方法调用、返回、异常处理都可能改变执行路径。

这些是 JVMS 规定的抽象执行模型。解释器可以按这个模型执行;JIT 也可以把相同逻辑转换为寄存器运算、条件跳转和其他机器指令,不需要继续保留原始的操作数栈形式。

四、解释器如何处理 JVM 字节码

1. 解释器的核心工作流程

JVM 规范第 2.11 节给出了一个简化的解释器执行模型:获取当前指令及其操作数,执行指令定义的操作,然后继续执行后续指令,直到方法完成或控制流发生改变。参见 JVMS §2.11。

从系统工程师的角度,可以把解释器的工作划分为四个步骤。

第一步:读取指令

根据当前执行位置读取字节码 opcode,并解析必要的操作数。例如,bipush 31 不只包含操作码,还包含整数常量 31。

第二步:分派指令处理逻辑

根据 opcode 找到对应的指令处理入口。例如,iadd 进入整数加法处理逻辑,if_icmple 进入条件比较与跳转逻辑。

第三步:执行指令语义

读取或修改局部变量、操作数栈和对象状态,执行算术运算、方法调用、异常检查等操作。

第四步:更新执行状态

更新当前指令位置,继续解释下一条指令;遇到方法返回、异常或控制流跳转时,则进入相应执行路径。

2. HotSpot 的模板解释器

在 OpenJDK HotSpot 中,一个关键实现是 Template Interpreter(模板解释器)。

它不是简单地让一个通用的 C++ 循环不断执行 switch(opcode)。HotSpot 会针对 JVM 字节码准备相应的处理模板,并由与 CPU 架构有关的代码生成逻辑创建解释器执行代码。

重点源码如下:

源码文件 研究重点
templateTable.cpp 定义字节码处理模板及相关指令分派信息
templateInterpreterGenerator.cpp 解释器代码的生成与入口组织
templateTable_x86.cpp x86 平台上的具体字节码处理实现
interpreterRuntime.cpp 解释器执行过程中需要的运行时辅助操作

这些链接指向 OpenJDK 主线源码。阅读时应结合具体 JDK 版本以及实际使用的 CPU 架构,不能把某一平台的实现直接等同于所有 JVM 的实现。

模板解释器的概念可以表示为下面的示意代码。这是为了便于理解而编写的伪代码,不是 OpenJDK 的原样源码。

java 复制代码
// 示意:模板解释器的基本工作方式。
// 实际 HotSpot 会使用生成的解释器代码及相应的指令分派机制。

while (method_has_more_bytecodes) {

    // 1. 读取当前 JVM 字节码。
    opcode = read_opcode(current_bytecode_position);

    // 2. 找到与该字节码关联的处理代码。
    handler = dispatch_table[opcode];

    // 3. 执行指令语义。
    // 例如 iadd 执行整数加法,if_icmple 执行条件分支。
    handler.execute();

    // 4. 继续执行;分支、返回及异常可能改变控制流。
}

这个模型对应的关键结论是:解释器分派的是 JVM 指令,而具体执行这些指令的解释器代码本身,也是由宿主 CPU 的机器指令构成的。

因此,"解释执行"不代表 CPU 不执行机器码。它代表 JVM 还在运行时处理 JVM 指令及其对应的解释器逻辑,而不是直接进入该 Java 方法经过 JIT 编译后得到的优化机器码。

3. 解释器为什么通常效率较低

以 result = product + y 为例,执行 iadd 时,解释器需要处理字节码对应的执行状态、操作数和指令分派。

如果方法非常简单,这些解释器开销相对于整数加法本身可能很显著。

解释器常见的性能开销包括:

  • 指令分派开销:需要选择并进入当前字节码的处理逻辑。
  • 运行时状态维护:需要维护与 JVM 抽象执行模型相对应的状态,包括局部变量、操作数栈和异常等信息。
  • 难以充分跨指令优化:单条指令的处理模式通常不具备优化编译器针对整个方法所掌握的完整优化机会。
  • 动态链接与运行时辅助调用:首次解析部分符号引用,或进行某些对象操作、同步和异常处理时,可能需要进入运行时辅助逻辑。

需要注意,现代 HotSpot 解释器也有优化机制,例如操作数栈顶缓存、常量池缓存以及快速字节码等。因此,不能简单地认为每一条字节码都会执行一次完整的 C++ 函数调用,或每次都访问内存中的操作数栈。

从系统工程角度来看,解释器的重要价值在于启动成本低、通用性强,并能够在运行过程中收集热点信息。它是运行时优化的重要基础,而不只是一个低性能的执行后备方案。

五、JIT 编译器如何处理字节码

1. JIT 的目标:从字节码生成优化机器码

JIT 是 Just-In-Time Compiler,即即时编译器。

解释器主要根据当前字节码执行相应的操作;JIT 则能够分析一个方法中的多个基本块、控制流和数据依赖,把字节码转化成中间表示,再进行优化,最终生成宿主 CPU 可以直接执行的机器码。

典型流程如下:

  1. 字节码解析

    读取方法的字节码、常量池引用和控制流信息。

  2. 构建中间表示

    建立基本块、控制流与数据依赖关系。

  3. 执行编译优化

    根据静态信息和运行时剖析信息,进行内联、常量传播等优化。

  4. 生成机器码

    进行指令选择、寄存器分配及相关代码生成。

  5. 安装编译结果

    将生成的代码安装到 JVM 管理的代码缓存中,并使其可以被执行。

HotSpot 的编译器并不是必须把每一个 JVM opcode 映射成一条独立的 CPU 指令。编译器可以重排、合并或消除操作,只要保持 Java 程序要求的可观察行为和 JVM 规范约束。

2. HotSpot 的 C1 与 C2

在常见的 HotSpot 分层编译模式下,JVM 不一定在程序启动时就把所有 Java 方法编译成高优化级别的机器码,而是根据运行时信息逐渐提升热点方法的执行级别。

编译级别 典型执行方式 主要目标
Level 0 解释器 快速开始执行,并可收集运行信息
Level 1 C1,较少剖析 快速编译,减少解释执行开销
Level 2 C1,有限剖析 平衡执行速度与信息收集
Level 3 C1,完整剖析 获取更丰富的运行时信息
Level 4 C2,优化编译 对热点代码进行更深入的优化

以上是常见分层编译模式的概念划分;实际编译级别会受构建方式、JVM 参数和编译策略影响。并非每个方法都严格按 Level 0 → 1 → 2 → 3 → 4 的顺序经历全部级别。

OpenJDK 的 compilationPolicy.hpp 对执行级别转换、调用计数、循环回边计数、编译队列及反馈调节机制有具体说明。其设计表明,编译策略不仅考虑方法调用次数,也会考虑循环执行次数以及编译器队列负载。

从职责上看:

  • C1 更强调较低的编译开销和较快的代码生成速度,也可以收集运行时剖析信息。
  • C2 面向更深入的优化,能够更充分地利用控制流和运行时类型等信息,优化经过反复执行的热点代码。

这是一种性能目标上的区分,不意味着 C1 完全不做优化,也不意味着 C2 对所有方法都一定更快。

3. JIT 如何判断一个方法是热点

JVM 的编译策略需要解决一个成本收益问题:

  • 如果一个方法只执行一次,将其编译成高度优化的机器码,可能得不偿失。
  • 如果一个方法在高频循环中反复执行,那么编译一次并重复使用生成的机器码,通常更有价值。

因此,HotSpot 会收集并利用运行时信息,例如方法调用计数、循环回边计数以及类型和分支剖析信息。

**循环回边(backedge)**是控制流回到循环中较早位置的边。在典型的热点循环中,即使方法只调用一次,循环体也可能被执行数百万次,所以仅统计方法调用次数并不足以判断其真实执行热度。

OpenJDK 编译策略相关源码:compilationPolicy.hpp。

其基本逻辑可以概括为以下带注释的伪代码:

cpp 复制代码
// 示意代码:HotSpot 热点检测的概念模型。
// 不是 OpenJDK 源码,也不是实际 JVM 固定采用的阈值。

if (methodInvocationCountExceeded()
        || loopBackedgeCountExceeded()) {

    // 提交编译请求,而不是在这里直接完成机器码生成。
    requestJitCompilation(method);
}

// 编译线程根据队列、当前编译级别和编译策略,
// 决定具体何时编译,以及使用哪个编译级别。

在 HotSpot 中,编译请求的协调和相关任务管理可以从 compileBroker.cpp 继续追踪。

要点是:达到热点条件不等于机器码已经生成,也不等于当前线程必然立即切换到新生成的机器码。 编译任务还可能需要排队、执行以及安装编译结果。

4. JIT 如何将字节码转化为中间表示

以 C2 为例,研究入口之一是:

src/hotspot/share/opto/parse1.cpp

这个文件中的解析逻辑负责处理方法入口、基本块和控制流等信息,并将字节码解析为编译器能够进一步处理的图结构。

考虑示例方法:

cpp 复制代码
static int calc(int x, int y) {
    int product = x * 31;
    int result = product + y;

    if (result > 100) {
        return result;
    }

    return result + 1;
}

从源代码到执行的过程中,可以把数据流理解为:

复制代码
Java 源码
   |
   v
JVM 字节码
   |
   v
C2 解析字节码,建立控制流和数据依赖
   |
   v
中间表示(IR)
   |
   +-- 常量传播
   +-- 代数化简
   +-- 控制流优化
   +-- 其他适用的优化
   |
   v
机器码生成

这里的 IR 是编译器的中间表示。它使编译器不必始终拘泥于字节码原先的栈式表达,而可以围绕数值、数据依赖和控制流进行优化。

例如,若编译器能够证明某个值恒定,便可能进行常量传播;如果可以证明某个检查没有必要,则有机会消除对应检查。至于某次具体编译是否实施优化,需要结合生成的编译日志、机器码及当时的运行时信息判断。

六、JIT 的关键优化机制

1. 方法内联(Inlining)

假设存在两个方法:

java 复制代码
static int add(int a, int b) {
    return a + b;
}

static int calculate(int x) {
    // 如果 JIT 决定将 add 内联,
    // 就可能把 add 的计算直接纳入调用者的优化过程。
    return add(x, 1);
}

解释器执行普通方法调用时,需要完成方法调用所需的运行时处理。

如果 JIT 将 add 内联到 calculate 中,编译器就可以在调用者上下文中分析 x + 1,并有机会继续优化后续计算。

内联并不只是消除一次调用开销。它的更大价值是扩大优化器能够整体分析的代码范围,为常量传播、死代码消除及其他跨调用优化创造条件。

不过,内联受到方法大小、调用频率、调用目标是否明确及编译预算等因素的影响,并非所有方法调用都会被内联。

2. 基于类型剖析的优化

下面的代码通过接口调用方法:

java 复制代码
interface Calculator {
    int calculate(int x);
}

static int run(Calculator calculator, int value) {
    return calculator.calculate(value);
}

若运行时剖析表明某个调用位置长期只观察到一个实现类型,JIT 可能据此进行推测性优化,例如将调用去虚拟化,使进一步内联成为可能。

但这种优化依赖运行时假设。未来如果出现此前没有观察到的实现类型,原有优化可能不再适用,JVM 必须通过保护性检查、去优化或重新编译等机制保证正确性。

因此,运行时剖析不是对程序未来行为的绝对保证,而是编译器优化决策的重要依据。

3. 其他常见优化

优化技术 系统工程师需要理解的作用
常量传播 利用已知常量简化表达式和分支
死代码消除 删除不会影响可观察结果的计算
范围检查消除 在条件成立时省去不必要的数组边界检查
循环优化 改善循环中的指令组织或减少重复计算
逃逸分析 分析对象是否可能被当前线程或作用域之外访问
标量替换 在符合条件时,将对象字段拆解为独立值,减少实际对象分配
锁消除 在能够证明同步操作不需要时,消除相关锁开销

以上优化并非必然发生。是否应用取决于代码形态、类型信息、编译级别、运行时状态和具体优化器能力。

七、JIT 优化后,JVM 如何保证程序语义正确

这是理解 JVMS 与 JIT 关系时最重要的部分。

JVMS 定义行为约束,JIT 优化的是实现方式,而不是改变程序必须遵守的语义。

在编译过程中,优化器需要考虑整数运算的语言语义、条件分支、方法返回、异常行为以及多线程程序的同步和内存模型约束。不能因为某种优化在单线程测试中看起来有效,就认为它必然合法。

例如,编译器可以把多条字节码合并为更少的机器指令,也可以使用寄存器保存中间结果,而不必按照原有栈操作逐条执行。但如果相关操作涉及 volatile、监视器或其他同步语义,编译器仍然必须保持规范要求的可观察行为。

从系统工程角度,理解优化是否正确,关键是研究哪些行为可观察、哪些变换满足语义约束、哪些推测需要运行时保护,而不是简单比较字节码数量与机器指令数量。

1. 去优化(Deoptimization)

假设 JIT 根据运行时剖析,推测一个调用点只会接收到某一种对象类型,并在此基础上进行了内联。

随后,运行时条件发生变化,原先的假设不再成立。JVM 不能继续无条件执行建立在失效假设上的优化代码。

HotSpot 可以通过保护性检查和去优化机制退出不再适用的编译代码,恢复出能够继续执行的 Java 执行状态,再使用解释器或其他适用的编译代码继续执行。

源码入口:

src/hotspot/share/runtime/deoptimization.cpp

这个过程不是普通意义上的"撤销所有已经执行的操作",而是利用编译时保存的调试信息、栈帧信息和相关状态,恢复正确的执行位置与必要状态。部分已完成的程序操作不能简单地被重新执行一遍,否则可能产生重复副作用。

学习总结:JIT 的推测性优化不是忽略正确性约束,而是将部分条件交给运行时检查。当条件失效时,通过去优化等机制回到正确的执行路径。

2. 栈上替换(OSR)

OSR 的全称是 On-Stack Replacement,即栈上替换。

考虑以下情况:

java 复制代码
static long calculate(long n) {
    long sum = 0;

    // 方法只调用一次,但循环可能执行很多次。
    for (long i = 0; i < n; i++) {
        sum += i;
    }

    return sum;
}

如果 JVM 等到 calculate 返回后才处理编译问题,就无法及时优化一个正在长时间运行的热点循环。

OSR 允许 JVM 为热点循环生成适当的编译入口,使正在解释执行的活动方法可以在满足条件时转入编译代码,而不必等到整个方法调用结束后再从头开始。

OSR 既体现了 JVM 以方法和循环热点为依据的优化思想,也涉及解释帧、编译帧之间的状态衔接。实际发生 OSR 需要满足编译策略及具体实现条件,并非每个热点循环都必然成功执行 OSR。

八、从系统工程师角度验证上述结论

理解源码之后,还需要通过实际运行数据验证执行模型,避免只凭理论推断性能。

1. 查看字节码

bash 复制代码
javac BytecodeDemo.java
javap -c -v BytecodeDemo

重点观察方法的 Code 属性、字节码指令、操作数栈最大深度、局部变量信息和异常表。对于需要进一步研究的方法,还可以核对方法描述符及常量池引用。

2. 观察编译事件

bash 复制代码
java -XX:+PrintCompilation BytecodeDemo

PrintCompilation 可以帮助观察方法是否发生编译、编译级别及相关编译事件。但输出中出现一个方法,并不意味着已经掌握其全部优化细节。

若希望进一步观察内联决策,可在支持这些诊断选项的 HotSpot 构建上使用:

bash 复制代码
java -XX:+UnlockDiagnosticVMOptions \
     -XX:+PrintCompilation \
     -XX:+PrintInlining \
     BytecodeDemo

这些选项主要用于观察与诊断,不应直接作为正式性能测试的标准配置。运行时参数及输出格式可能随 JDK 版本发生变化。

3. 比较解释执行与编译执行

可以通过以下模式辅助研究:

bash 复制代码
# 仅使用解释器,适合研究解释执行路径
java -Xint BytecodeDemo

# 允许通常的分层编译行为
java BytecodeDemo

# 强制积极采用编译执行策略,仅适合诊断和实验
java -Xcomp BytecodeDemo

-Xcomp 不等价于真实生产环境中的稳定热点编译过程,不应该简单地用它与默认模式的运行时间差来判断 JIT 的最终收益。要比较性能,应设计可重复的基准测试,考虑启动时间、预热、垃圾回收、编译耗时、CPU 频率变化以及程序输入分布等因素。

对于更深入的分析,可以进一步研究:

  • OpenJDK 的 compileBroker.cpp:编译任务如何进入编译系统。
  • OpenJDK 的 compilationPolicy.hpp:执行级别如何转换,以及热点策略如何受队列负载影响。
  • OpenJDK 的 opto/parse1.cpp:C2 如何解析方法并建立中间表示。
  • OpenJDK 的 runtime/deoptimization.cpp:如何处理编译代码退出和执行状态恢复。

以上源码链接均指向 OpenJDK 主线,实际学习时应切换到与目标运行环境相匹配的 JDK 标签或提交版本。

九、最终学习总结

从系统工程师的角度,解释器和 JIT 的区别不仅是"一个解释、一个编译",更重要的是它们在 JVM 执行体系中承担的职责不同。

解释器根据 JVMS 规定的指令语义执行字节码,维护方法执行状态,并为运行时剖析和热点检测提供基础。

JIT 编译器通过分析热点方法或循环,将字节码转换为中间表示,并结合静态信息与运行时反馈进行优化,最终生成 CPU 可直接执行的本地机器码。

HotSpot 运行时系统负责在解释执行、不同编译级别、OSR 和去优化等路径之间进行协调,使程序能够在正确性约束下利用运行时信息提高执行效率。

学习时建议牢牢抓住三条主线:第一,JVMS 规定了哪些语义不可改变;第二,HotSpot 通过哪些解释器及编译器实现这些语义;第三,运行时计数、编译队列、机器码安装和去优化如何共同影响程序真实的执行性能。

这三条主线贯通之后,就能从"理解某条 JVM 字节码"进一步深入到"理解某段 Java 代码在真实 JVM 和 CPU 上为何这样运行"。

十、解释器与 JIT 编译器的源码对照总结

为了把规范与实现对应起来,建议采用"规范条款 → Java 示例 → 字节码 → HotSpot 源码 → 运行时验证"的学习方法。

1. 规范层与实现层的区别

对比维度 JVMS 规范 HotSpot 实现
执行对象 JVM 字节码 字节码、解释器代码、中间表示和本地机器码
执行模型 定义指令语义、栈帧和控制流 可以使用模板解释器及 JIT
性能优化 允许实现采用不同优化方法 根据运行时剖析和编译策略进行优化
执行代码 不规定必须采用某种形式 可执行解释器生成的机器码或 JIT 编译后的机器码
优化假设失效 要满足规范要求的行为 可借助保护检查、去优化等机制恢复正确执行

JVMS 明确将很多内部实现细节留给具体的 JVM 实现,包括将字节码翻译成本地机器码。

2. 推荐的源码阅读顺序

第一阶段:理解规范

阅读 JVMS 第 2 章:JVM 结构 与 第 6 章:指令集。

重点:Frame、局部变量表、操作数栈、指令语义与控制流。

第二阶段:分析解释器

阅读 templateTable.cpp、templateInterpreterGenerator.cpp。

重点:字节码处理模板、解释器入口和指令分派。

第三阶段:分析 JIT 编译

阅读 compilationPolicy.hpp、compileBroker.cpp 与 parse1.cpp。

重点:热点判断、编译任务调度与 C2 字节码解析。

第四阶段:分析执行状态转换

阅读 deoptimization.cpp。

重点:优化假设失效时,如何恢复解释执行所需的状态。

3. 最小实验:验证字节码与编译行为

下面给出一个可直接编译运行的 Java 示例。代码中的注释说明了需要重点观察的行为。

java 复制代码
public final class BytecodeDemo {

    // 研究目标:理解 iload、iadd、ireturn 三条指令的语义。
    static int sum(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        long checksum = 0;

        // 高频调用,为 JIT 热点检测和优化提供观察对象。
        for (int i = 0; i < 10_000_000; i++) {
            checksum += sum(i, 1);
        }

        System.out.println(checksum);
    }
}

先查看字节码:

bash 复制代码
javac BytecodeDemo.java
javap -c -v BytecodeDemo

sum 方法的核心字节码应类似于:

复制代码
iload_0   // 将参数 a 加载到操作数栈
iload_1   // 将参数 b 加载到操作数栈
iadd      // 弹出两个 int,执行加法,再将结果压栈
ireturn   // 返回 int 结果

再观察实际编译事件:

bash 复制代码
# 观察常规 JVM 执行及编译行为
java -XX:+PrintCompilation BytecodeDemo

# 在支持相应诊断选项的 HotSpot 上观察内联决策
java -XX:+UnlockDiagnosticVMOptions \
     -XX:+PrintCompilation \
     -XX:+PrintInlining \
     BytecodeDemo

还可以用以下模式进行辅助对照:

bash 复制代码
# 以解释器执行为主,研究解释路径
java -Xint BytecodeDemo

# 积极采用编译执行策略,适合诊断实验
java -Xcomp BytecodeDemo

实验时需要注意:sum 可能被内联,因而不能期待始终看到一个独立的 sum 机器码调用。-Xcomp 也不等价于默认分层编译下经过充分预热的生产运行状态。正式性能比较应控制预热、输入、垃圾回收及测试环境。

十一、最终结论

解释器解决的是"如何执行字节码",JIT 编译器解决的是"如何让热点字节码更高效地执行"。

解释器按照 JVM 指令语义执行操作,并维护运行时执行状态;JIT 则通过解析字节码、构建中间表示、应用优化和生成机器码,减少热点代码的重复解释开销。HotSpot 还通过热点检测、分层编译、OSR 和去优化等机制,协调执行效率与程序语义正确性。

作为系统工程师,最重要的不是机械记忆某一条字节码对应哪条 CPU 指令,而是能够从真实源码和运行数据回答三个问题:为什么这段字节码这样执行、JIT 为什么认为这段代码值得优化,以及优化前后 JVM 如何继续保证正确的执行语义。

相关推荐
txxzjmzlh2 小时前
SpringBoot 配置⽂件
java·spring boot·spring
小徐不徐说2 小时前
序列化与反序列化:把 C++ 对象可靠地保存下来
开发语言·数据库·c++·php
ragnwang2 小时前
Notes: Python pybind11 import C++ module
开发语言·c++·python
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 搜索附近的功能概述和分析
java·开发语言·交友
骑着蜗牛撵大象3272 小时前
大模型 Agent 的沙箱护栏:gVisor 隔离、出口白名单与审计闭环
java·开发语言·jvm·安全·agent沙箱·gvisor·microvm
IMPYLH2 小时前
Linux 的时间为什么会被拨回去
linux·运维·服务器
独泪了无痕2 小时前
Hutool之LocalDateTimeUtil:让时间间隔操作如虎添翼
java
充值内卷3 小时前
我给桌面加了个鱼缸:C++ 写后端,WebView2 里养一座 Three.js 小岛
开发语言·javascript·c++
三无少女指南4 小时前
从这里开始:Java 后端与 AI 工程的三条阅读路线
java·学习路线·ai工程