本页目录
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 次,每次都同一个类型"这种场景:
这不只是在编译器里做——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