Skip to content

07.1 - 解释执行与编译执行

定位: JVM 执行引擎的两种工作模式——解释器保证快速启动,编译器保证峰值性能,二者通过混合模式共存 面试高频度: ⭐⭐⭐⭐ 考查方式: 为什么需要两种执行方式、热点检测机制、OSR 原理、CompileThreshold

一、这是什么?为什么需要它?

执行引擎将字节码转换为机器码。问题在于:纯解释太慢,纯编译启动太慢。

纯解释模式 (只有解释器):
启动速度: ★★★★★  几乎无延迟
峰值性能: ★       每条字节码逐条翻译, 约 1/10 ~ 1/100 编译后速度
内存消耗: ★       无需存储编译代码
                → 适合短生命周期应用 (一次性命令行工具)

纯编译模式 (只有编译器):
启动速度: ★       JVM 启动时编译所有方法, 等待时间不可接受
峰值性能: ★★★★★   编译后接近 native 代码速度
内存消耗: ★★★★★   所有方法编译代码存入 CodeCache
                → 适合长时间运行且不关心启动时间的应用

混合模式 (HotSpot 方案):
启动速度: ★★★★    解释器立即执行, 编译器后台编译热点
峰值性能: ★★★★★   热点代码达到 C2 优化级别后接近 native
内存消耗: ★★★     仅编译热点, 冷代码继续解释执行
                → 鱼和熊掌兼得!

核心理念:大多数代码只执行一次(冷代码),不值得编译。少数热点代码执行了 99% 的计算量,值得花时间深度优化。混合模式让"冷代码跑得慢但启动快,热代码跑得飞快"。

二、原理拆解

2.1 解释执行 (Interpretation)——逐门翻译

JVM 解释器(模板解释器)为每条字节码指令预先生成了一段对应的 native 代码模板:

┌─────────────────────────────────────────────────────────────────────┐
│                       模板解释器工作方式                                 │
│                                                                     │
│  字节码流:                                                           │
│  ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐                │
│  │aload │iload │invoke│aretur│astore│iload │ifge  │                │
│  │_0    │_1    │virt. │n     │_2    │_1    │      │                │
│  └──┬───┴──┬───┴──┬───┴──┬───┴──┬───┴──┬───┴──┬───┘                │
│     │      │      │      │      │      │      │                      │
│     ▼      ▼      ▼      ▼      ▼      ▼      ▼                      │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐                  │
│  │取模│ │取模│ │取模│ │取模│ │取模│ │取模│ │取模│                  │
│  │板1 │ │板2 │ │板3 │ │板4 │ │板5 │ │板6 │ │板7 │                  │
│  │→执 │→执 │→执 │→执 │→执 │→执 │→执 │                  │
│  │行  │ │行  │ │行  │ │行  │ │行  │ │行  │ │行  │                  │
│  └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘                  │
│                                                                     │
│  每条字节码 → 跳转到对应 native 代码模板 → 执行 → 取下一条字节码         │
│  (类似 CPU 的取指 → 译码 → 执行 循环)                                 │
└─────────────────────────────────────────────────────────────────────┘

优势

  • 无编译延迟——字节码被读取即可执行
  • 内存占用低——不需要存储编译结果
  • 实现简单且稳定——模板解释器代码量小,bug 少

劣势

  • 性能差——每个 bytecode 都要经过"取指 → 分派 → 执行"循环
  • 无法跨指令优化——逐个执行,看不到更全局的优化机会

2.2 编译执行 (JIT)——整章翻译

JIT(Just-In-Time) 编译器将"热点"方法整体翻译为优化后的机器码:

┌─────────────────────────────────────────────────────────────────────┐
│                   JIT 编译执行工作方式                                    │
│                                                                     │
│  ┌──────────────────┐                                               │
│  │ 热点方法 foo()    │                                               │
│  │ (字节码 ~200行)   │                                               │
│  └────────┬─────────┘                                               │
│           │                                                          │
│           ▼ 编译 (C1 或 C2)                                          │
│  ┌────────────────────────────────────────────────┐                  │
│  │ 优化后的机器码 (foo() 的 native 代码 ~2000 行)    │                  │
│  │ - 方法内联                                       │                  │
│  │ - 循环展开                                       │                  │
│  │ - 逃逸分析                                       │                  │
│  │ - 边界检查消除                                   │                  │
│  │ - 常量折叠                                       │                  │
│  │ ...                                             │                  │
│  └────────────────────────────────────────────────┘                  │
│           │                                                          │
│           ▼ 执行                                                    │
│  CPU 直接执行优化后的机器码 —— 无需再经过字节码解释层!                  │
└─────────────────────────────────────────────────────────────────────┘

优势

  • 性能极高——C2 编译代码可达 native C++ 的 80%-100% 性能
  • 跨方法优化——内联后可以看到更大范围的优化机会
  • 基于 profiling 的投机优化——利用运行时数据做最优假设

代价

  • 编译本身消耗 CPU——C2 编译一个方法可能需要 1 秒以上
  • CodeCache 消耗——编译后的机器码需要存储空间
  • 去优化开销——投机假设不成立时需要回退

2.3 混合模式与热点检测

HotSpot 启动时默认使用混合模式 (-Xmixed),这是 JDK 8 的默认配置:

时间线: 方法 foo() 的"人生"
───────────────────────────────────────────────────────────────────────

T0: 方法首次调用


解释执行阶段: foo() 在解释器中运行
    │  方法调用计数器: 0 → 1 → 2 → ... → 1000 → ...
    │  回边计数器:    (循环每迭代一次 + 1)

    ▼ 计数器达到 -XX:CompileThreshold = 10000 (Server 模式)

编译请求入队: foo() 被加入编译队列
    │  解释器继续执行 foo() (编译在后台进行)

    ▼ C2 编译完成

OSR 替换: 如果 foo() 正在执行(如在循环中)→ 栈帧被替换为编译版本
    │  注意: 编译完成时 foo() 可能正在解释执行中!
    │  OSR(On-Stack Replacement) 让正在执行的栈帧"热替换"成编译版本


编译执行阶段: 后续调用 foo() 直接跳转到编译后的 native 代码
    │  CodeCache 中存储编译结果
    │  方法入口被重定向(从解释入口 → 编译入口)

    ▼ 如果计数器衰减...

解释执行回归: 方法长时间不调用 → 计数器衰减 → 编译代码可能被卸载

计数器机制详解

方法调用计数器 (Method Invocation Counter):
  - 每调用一次 + 1
  - 阈值: -XX:CompileThreshold (Server 默认 10000, Client 默认 1500)
  - 达到阈值 → 提交编译请求

回边计数器 (Back Edge Counter):
  - 循环每迭代一次 + 1
  - 阈值: -XX:CompileThreshold * OnStackReplacePercentage / 100
    (Server: 10000 * 140 / 100 = 14000)
  - 达到阈值 → 提交 OSR 编译请求

计数器衰减 (Counter Decay):
  - 每次 GC 时, 计数器值减半
  - 防止"瞬时热点"浪费编译资源
  - 衰减阈值: -XX:CompileThreshold / 2 (低于此值的方法被视为冷方法)

2.4 OSR (On-Stack Replacement)

OSR 是 JIT 编译中最精妙的技术之一。要解决的问题:一个长循环开始执行的时候还没被编译,循环体执行到一半时编译完成了——如何切换?

OSR 场景:
┌─────────────────────────────────────────────────┐
│ public void longLoop() {                         │
│     // 这个方法还在解释执行...                       │
│     for (int i = 0; i < 1_000_000; i++) {       │
│         // ⚡ 回边计数器累积到触发 OSR 编译          │
│         // ⚡ C2 开始编译 longLoop()               │
│         // 😱 编译完成了但我在循环中间!              │
│         heavyComputation(i);                     │
│     }                                             │
│ }                                                 │
└─────────────────────────────────────────────────┘

OSR 如何解决

  1. 编译时在循环开始处插入一个"OSR 入口点"
  2. 编译完成后,JVM 将当前解释执行栈帧的状态(局部变量、操作数栈)转换为编译代码需要的状态
  3. 线程跳转到 OSR 入口点,从当前循环迭代继续执行

关键理解:OSR 编译的代码通常不如正常编译的代码优化得好——因为 C2 需要在循环中间切入,能做优化的范围受限。OSR 主要用于快速提升长循环的性能,后续正常入口的调用会使用非 OSR 版本。

三、图解全景

┌──────────────────────────────────────────────────────────────────────────┐
│                  混合模式执行时间线                                          │
│                                                                          │
│  CPU 时间                                                               │
│      │  方法 A (冷, 执行一次)                  方法 B (热, 频繁调用)         │
│      │  ┌──────────────────┐                  ┌────────────────────────┐ │
│      │  │ 解释执行          │                  │ 解释执行                │ │
│      │  │ ~2ms             │                  │ ~50ms (调用了5000次)   │ │
│      │  └──────────────────┘                  └──────────┬─────────────┘ │
│      │                                                   │                │
│      │                                                   ▼                │
│      │                                           ┌────────────────┐      │
│      │                                           │ 编译请求入队    │      │
│      │                                           │ C2 编译(300ms) │      │
│      │                                           └───────┬────────┘      │
│      │                                                   │                │
│      │                                                   ▼                │
│      │     方法 C (极热, 长循环)                   ┌────────────────┐      │
│      │     ┌─────────────────┐                    │ 编译后执行      │      │
│      │     │ 解释执行(循环)   │                    │ 剩余 ~95000次   │      │
│      │     │ 回边计数器触发    │                    │ 每次 ~0.02ms   │      │
│      │     │ OSR 编译请求     │                    │ 总 ~1.9ms      │      │
│      │     └────────┬────────┘                    └────────────────┘      │
│      │              ▼                                                     │
│      │     ┌────────────────┐                                             │
│      │     │ OSR 编译(500ms) │                                            │
│      │     │ 编译完成后      │                                             │
│      │     │ 栈帧替换        │                                             │
│      │     └───────┬────────┘                                             │
│      │              ▼                                                     │
│      │     ┌────────────────┐                                             │
│      │     │ 编译版本继续    │                                             │
│      │     │ 执行循环       │                                             │
│      │     │ 速度 ~10x      │                                             │
│      │     └────────────────┘                                             │
│      └──────────────────────────────────────────────────────────────────  │
│                                                                          │
│  核心收益: 冷方法 (A) 只花 ~2ms 无编译代价;                                        │
│  热方法 (B) 花 300ms 编译但后续调用快了 ~50x;                                      │
│  极热方法 (C) OSR 编译后在循环后半段提速 ~10x                                      │
│  总体: 启动时间短, 长期吞吐高                                                   │
└──────────────────────────────────────────────────────────────────────────┘

四、实战验证

bash
# 查看 JVM 是否运行在混合模式
java -version

# 输出:
# java version "1.8.0_..."
# Java HotSpot(TM) 64-Bit Server VM (build 25.x-bxx, **mixed mode**)
#                                                      ^^^^^^^^
bash
# 查看 JIT 编译活动
# -XX:+PrintCompilation 方法每被编译一次就输出一行
java -XX:+PrintCompilation -version

# 输出 (截取关键行):
#     59    1       3       java.lang.String::hashCode (55 bytes)
#     60    2       3       java.lang.String::equals (50 bytes)
#     61    3       3       java.lang.String::indexOf (70 bytes)
#     89   32       4       java.lang.Object::<init> (1 bytes)
#    105   48       4       java.lang.String::hashCode (55 bytes)
#     ^    ^       ^       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
#     |    |       |       └── 方法签名
#     |    |       └────────── 编译层级 (3 = C1 full, 4 = C2)
#     |    └────────────────── 编译 ID (全局递增)
#     └─────────────────────── 时间戳 (ms 或编译队列开始后时间)
bash
# 控制编译行为的关键参数
# 设置编译阈值
-XX:CompileThreshold=5000       # 方法调用 5000 次后编译 (Server 默认 10000)
-XX:+PrintCompilation            # 打印编译日志
-XX:-UseCompiler                 # 完全禁用 JIT (仅解释执行 — 极慢!)
-Xint                           # 纯解释模式(等效于 -XX:-UseCompiler)
-Xcomp                          # 纯编译模式(所有方法都编译 — 启动慢!)

五、面试视角

追问答案要点
为什么需要解释器和编译器共存?纯解释启动快但性能差,纯编译性能好但启动慢。混合模式:启动时解释执行,识别热点后编译热点代码,兼顾启动速度和峰值性能。
什么代码会被 JIT 编译?"热点代码"——频繁执行的方法(方法调用计数器达 CompileThreshold)或长循环(回边计数器达 OSR 阈值)。冷代码始终在解释器运行。
什么是 OSR?有什么用?On-Stack Replacement。当一个方法在解释执行长循环时编译完成,OSR 允许正在执行的栈帧"热替换"为编译版本,从当前循环位置继续执行。
CompileThreshold 怎么影响性能?默认 10000(Server)。设小值 → 更快编译 → 更快达到峰值性能,但可能编译冷代码浪费资源。设大值 → 更慢达到峰值性能,但节省编译资源和 CodeCache。
计数器衰减的作用是什么?防止"瞬时热点"浪费编译资源。GC 时计数器减半,持续不热的"假热点"会被衰减到阈值之下,不再触发编译。
解释执行一定比编译执行慢多少?约 10-100 倍不等。取决于字节码复杂度、JIT 优化程度。简单的 getter/setter 可能只慢 2-3 倍,复杂的多态调用链可能差 100 倍以上。
-Xint 和 -Xcomp 有什么区别?-Xint 纯解释,JIT 完全禁用,无编译开销但速度极慢。-Xcomp 所有方法都编译,启动极慢但运行快。生产环境永远不要用这两个参数。

📚 相关链接

  • **字节码指令集概览** — 字节码是解释器执行的基本单位
  • **C1与C2编译器与分层编译** — 编译器如何去编译热点代码
  • **方法内联和逃逸分析** — C2 最重要的两项优化
  • ← 返回 **执行引擎索引**

Knowledge4J — Java 知识库