---
title: JIT 编译
url: https://doc.liz6.com/compilers/08-compilation-technology-applications/01-jit-compilation
locale: zh
area: compilers
tags:
- compilers
- 编译技术应用
date: 2026-06-30
modified: 2026-07-11
description: 在运行时把字节码编译成机器码。分层编译用 baseline 保启动速度,用优化 JIT 提峰值性能;inline caching 和推测+反优化让 JIT 敢做 AOT 不敢做的激进优化——推测失败就退回到解释执行。
---

# JIT 编译

> 在运行时把字节码编译成机器码。分层编译用 baseline 保启动速度,用优化 JIT 提峰值性能;inline caching 和推测+反优化让 JIT 敢做 AOT 不敢做的激进优化——推测失败就退回到解释执行。

## 概述

传统编译(AOT)在程序运行前生成全部机器码。JIT(Just-In-Time)在**运行时**将字节码或 IR 编译成机器码——只有在"这段代码确实被调用了"时才编译它。这带来两个独特优势:**运行时信息**(知道哪个分支真的热、哪个类型真的被用),和**不需要编译没跑到的代码**(缩小编译范围、加速启动)。代价是编译时间算在运行时,所以 JIT 设计的核心矛盾是"编译质量 vs 编译开销"——这一篇讲 JIT 如何用**分层编译**和**推测+反优化**来解决这个矛盾。

## 分层编译:不止一个 JIT,是几个互补

现代 JIT 不止一个编译器,而是分层的:

```
baseline/JIT (Ignition/V8, 或 C1/HotSpot):
  → 快速编译, 几乎零优化
  → 每个函数只编译一次, 编译时间 << 解释执行节省的时间

优化 JIT (TurboFan/V8, 或 C2/HotSpot):
  → 仅对"热点"函数 (被调用了很多次, 或循环迭代很多轮)
  → 做全套优化: inlining, GVN, LICM, loop unrolling, SROA
  → 编译时间长, 但热点函数运行时占比大, 值得
```

调用计数和循环回边计数决定"该不该升层":

```
counter[function] += 1                                  ← 每次调用加一
if counter[function] >= threshold_baseline:
    编译为 baseline code

backedge_counter[loop] += 1                             ← 每次回边加一
if backedge_counter[loop] >= threshold_optimized:
    进优化编译器
```

阈值是动态调的(V8 的 TurboFan 根据启动阶段放宽阈值,让冷启动快;进入稳态后降低阈值,让热点更快被优化)。

## Inline Caching:多态调用的快速路径

JIT 最核心的优化之一,针对"同一条 `obj.method()` 调用了 1000 次,每次都同一个类型"这种场景:

<svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="普通调用路径与内联缓存路径对比">
  <rect width="720" height="320" fill="#ffffff"/>
  <defs>
    <marker id="jit-ic-ah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">普通路径 vs 内联缓存(IC)路径</text>

  <text x="40" y="86" font-size="13" font-weight="700" fill="#64748b">普通路径</text>
  <text x="40" y="100" font-size="11" fill="#94a3b8">(无 IC)</text>
  <rect x="118" y="68" width="120" height="38" rx="5" fill="#e2e8f0"/>
  <text x="178" y="92" text-anchor="middle" font-size="11.5" fill="#334155">查 obj 的类型</text>
  <line x1="238" y1="87" x2="260" y2="87" stroke="#475569" stroke-width="1.6" marker-end="url(#jit-ic-ah)"/>
  <rect x="264" y="68" width="176" height="38" rx="5" fill="#e2e8f0"/>
  <text x="352" y="92" text-anchor="middle" font-size="11.5" fill="#334155">类型表查 method 偏移</text>
  <line x1="440" y1="87" x2="462" y2="87" stroke="#475569" stroke-width="1.6" marker-end="url(#jit-ic-ah)"/>
  <rect x="466" y="68" width="120" height="38" rx="5" fill="#e2e8f0"/>
  <text x="526" y="92" text-anchor="middle" font-size="11.5" fill="#334155">跳转到偏移</text>

  <text x="40" y="196" font-size="13" font-weight="700" fill="#0f766e">内联缓存路径</text>
  <text x="40" y="210" font-size="11" fill="#5eead4">(有 IC)</text>
  <rect x="118" y="172" width="150" height="46" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="193" y="191" text-anchor="middle" font-size="11" font-weight="700" fill="#115e59">obj.type ==</text>
  <text x="193" y="206" text-anchor="middle" font-size="11" font-weight="700" fill="#115e59">prev_type ?</text>

  <line x1="268" y1="182" x2="304" y2="140" stroke="#0d9488" stroke-width="1.6" marker-end="url(#jit-ic-ah)"/>
  <text x="292" y="150" font-size="10.5" fill="#0d9488">命中 &gt;95%</text>
  <rect x="308" y="118" width="230" height="40" rx="6" fill="#0d9488"/>
  <text x="423" y="136" text-anchor="middle" font-size="11.5" font-weight="700" fill="#ffffff">直接跳到 prev_offset 代码</text>
  <text x="423" y="151" text-anchor="middle" font-size="10.5" fill="#ccfbf1">fast path</text>

  <line x1="268" y1="206" x2="304" y2="228" stroke="#f97316" stroke-width="1.6" marker-end="url(#jit-ic-ah)"/>
  <text x="292" y="222" font-size="10.5" fill="#c2410c">未命中</text>
  <rect x="308" y="208" width="160" height="40" rx="6" fill="#ffedd5"/>
  <text x="388" y="225" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">查类型表</text>
  <text x="388" y="240" text-anchor="middle" font-size="10.5" fill="#c2410c">(慢路径 slow path)</text>
  <line x1="468" y1="228" x2="490" y2="228" stroke="#475569" stroke-width="1.6" marker-end="url(#jit-ic-ah)"/>
  <rect x="494" y="208" width="190" height="40" rx="6" fill="#ffedd5"/>
  <text x="589" y="225" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">更新 prev_type</text>
  <text x="589" y="240" text-anchor="middle" font-size="10.5" fill="#9a3412">prev_offset</text>

  <rect x="40" y="272" width="640" height="32" rx="8" fill="#eef2ff"/>
  <text x="360" y="292" text-anchor="middle" font-size="12" fill="#3730a3">fast path 只需 2–3 条指令(类型比较+条件跳转),命中率通常 &gt;95%——本质是把"类型和上次一样"当推测来做</text>
</svg>

这不只是在编译器里做——**inline cache 生成为机器码中的数据+代码组合**,每次调用来时只做一次类型比较 + 条件跳转(2–3 条指令),命中率通常 >95%。fast path 命中时,相当于把这 2–3 条指令当作一个**推测(speculation)**:推测这次调用的类型和上次一样。inline caching 是多态调用的典型推测优化。

## 推测 + Guards + 反优化:JIT 的独特武器

AOT 编译器不知道运行时的类型分布,只能生成保守代码。JIT 看到"这个变量 1000 次里 999 次是 `int`"后,可以**推测**它是 `int`,生成最优路径 + guard:

```
guard: if (typeof(x) != INT) goto deopt_entry      ← guard
fast_code: return x + 1                             ← 推测路径

deopt_entry:
    保存当前状态到栈帧 (on-stack replacement)
    切回解释器或 baseline code
    在解释器里重做这一操作
```

关键机制是**反优化(deoptimization)**:当推测被打破时,JIT 必须把当前优化帧的状态(寄存器、局部变量)映射回"未优化 IR 在这一点该有的状态",然后跳回解释器或 baseline 继续执行。这要求编译器维护**从优化代码到未优化代码的映射表**(deoptimization metadata)——每个优化点在未优化 IR 的哪个位置,每个优化变量对应哪个未优化变量。

### On-Stack Replacement(OSR)

反优化的特例:函数正在执行循环(可能跑了 10000 次),此时触发优化或反优化,需要在**栈帧中间**把执行状态从旧版本转移到新版本——这就是 OSR。OSR 要求 JIT 能"把正在跑的代码的栈帧转译成目标代码期望的栈帧"——这依赖 deopt metadata 的完备性。

## 代码缓存与碎片

JIT 编译的代码存在 **code cache** 里。和 GC 管理对象类似,code cache 也需要管理:

- **空间**:编译太多函数,code cache 爆满 → 丢弃不太热的编译后代码(Code Cache Flush),回退到解释执行或 baseline。
- **碎片**:编译后的函数大小不一致,频繁 flush 产生碎片 → code cache 用 slab 分配器(类似 malloc 的 arena),对常用尺寸做预切分。

## 分层与 AOT 的边界越来越模糊

现代运行时很多已不是纯 JIT:

- **Android ART**:安装时把 dex 字节码 AOT 编译成机器码(利用安装时间),运行时只对热点做 JIT 补充。
- **GraalVM Native Image**:AOT 全量编译到 native binary —— 无 JIT,但用**推测优化**(如果知道类层次封闭,就 devirtualize)。
- **Wasm 的 JIT**:浏览器里跑 Wasm,先做快速 baseline 编译(几毫秒),然后对热点做优化 JIT(几十毫秒)——和 JS JIT 是同一套引擎。

JIT 和 AOT 的共性是:**编译器 = 从一种表示翻译到一种更接近机器的表示**,区别只在"翻译发生在何时"。分层编译把这个时间拆成多层,就是让每种翻译在最适合它的时刻发生。

## 权衡与失败模式

- **编译时间吃掉收益**:短期脚本只跑一次,编译比解释还慢 → 用分层,只对热点做重编译,宁可先解释。
- **推测过激**:类型推测在离奇输入上连续 miss → 反优化风暴(不断 deopt→recompile→deopt)——监控 deopt 率,超过阈值停止对该函数重优化。
- **code cache 爆满**:长进程里不断编译新代码 → 设 code cache 上限,LRU 淘汰,满时降级到 baseline/解释。
- **调试地狱**:JIT 的机器码和源码之间的映射是动态的 — deopt 改变栈帧,inline 改变调用栈 → 调试信息必须把"优化后的调用栈"映射回"源码层面的调用栈"(JVM 的 `-XX:+PrintDeoptimizationDetails`、V8 的 `--trace-deopt`)。

## 参考

- **V8**: Ignition(解释器) + Sparkplug(baseline JIT) + Maglev/TurboFan(优化 JIT) — V8 博客有每层的设计文档
- **JVM**: HotSpot C1(client compiler) + C2(server compiler), `-XX:+PrintCompilation` 观察分层
- **Aycock (2003)**: "A Brief History of Just-In-Time" — JIT 早期的学术综述

*Keywords: JIT, just-in-time, method JIT, tracing JIT, tiered compilation, baseline compiler, optimizing compiler, hotspot, inline caching, hidden class, speculation, guard, deoptimization, on-stack replacement, OSR, deopt metadata, code cache, PGO, profile-guided optimization, AOT*
