第2章:JIT 工作原理
第2章:JIT 工作原理
“JIT 编译器的精妙之处,在于它能在程序运行的同时,悄无声息地完成优化。”
2.1 JIT 编译的整体架构
一个典型的 JIT 编译系统包含以下核心组件:
源代码 / 动态语言代码
│
▼
┌─────────────┐
│ 解释器 │ ← 初始执行路径
│ (Interpreter)│
└──────┬──────┘
│ 收集 Profile 数据
▼
┌─────────────┐
│ 热点检测器 │ ← 识别值得优化的代码
│ (Profiler) │
└──────┬──────┘
│ 触发编译
▼
┌─────────────┐
│ 前端 │ ← 解析、生成 IR
│ (Frontend) │
└──────┬──────┘
│
▼
┌─────────────┐
│ 优化管线 │ ← 多趟优化
│ (Optimizer) │
└──────┬──────┘
│
▼
┌─────────────┐
│ 后端 │ ← 生成机器码
│ (Backend) │
└──────┬──────┘
│
▼
┌─────────────┐
│ 代码缓存 │ ← 存储生成的机器码
│ (Code Cache) │
└─────────────┘
2.2 字节码与解释执行
2.2.1 什么是字节码
字节码(Bytecode)是源代码编译后的中间表示,比源码低级但比机器码高级。它设计为解释器可以高效执行的格式。
// Java 源码
public int add(int a, int b) {
int result = a + b;
return result;
}
// 对应字节码 (javap -c)
// 0: iload_1 // 加载局部变量 1 (参数 a)
// 1: iload_2 // 加载局部变量 2 (参数 b)
// 2: iadd // 整数加法
// 3: istore_3 // 存储到局部变量 3 (result)
// 4: iload_3 // 加载 result
// 5: ireturn // 返回整数
2.2.2 不同语言的字节码格式
| 语言 | 字节码格式 | 特点 |
|---|---|---|
| Java | JVM Bytecode | 栈式架构,类型丰富 |
| Python | CPython Bytecode | 栈式架构,动态类型 |
| C# | CIL (Common Intermediate Language) | 栈式架构,面向对象 |
| Lua | Lua Bytecode | 寄存器式架构 |
| JavaScript | V8 Bytecode (Ignition) | 寄存器式架构 |
2.2.3 自定义字节码设计示例
// 简单虚拟机的字节码定义
typedef enum {
OP_LOAD_CONST, // 加载常量
OP_LOAD_VAR, // 加载变量
OP_STORE_VAR, // 存储变量
OP_ADD, // 加法
OP_SUB, // 减法
OP_MUL, // 乘法
OP_DIV, // 除法
OP_CALL, // 函数调用
OP_RETURN, // 返回
OP_JUMP, // 无条件跳转
OP_JUMP_IF_FALSE, // 条件跳转
} Opcode;
// 字节码指令结构
typedef struct {
uint8_t opcode;
uint16_t operand;
} Instruction;
// 解释器循环
Value execute(VM* vm, Function* func) {
Instruction* ip = func->code; // 指令指针
Value stack[256]; // 操作数栈
int sp = 0; // 栈指针
for (;;) {
switch (ip->opcode) {
case OP_LOAD_CONST:
stack[sp++] = func->constants[ip->operand];
break;
case OP_ADD: {
Value b = stack[--sp];
Value a = stack[--sp];
stack[sp++] = a + b;
break;
}
case OP_RETURN:
return stack[--sp];
// ... 其他指令
}
ip++;
}
}
2.3 中间表示(IR)
2.3.1 IR 的作用
中间表示(Intermediate Representation,IR)是编译器内部使用的代码表示形式,连接前端和后端。
源代码 → [前端] → IR → [优化器] → 优化IR → [后端] → 机器码
(解析) (多趟优化) (代码生成)
2.3.2 常见 IR 类型
| IR 类型 | 代表 | 特点 |
|---|---|---|
| AST | 早期编译器 | 保留源码结构 |
| 三地址码 | GCC | 简单、易优化 |
| SSA | LLVM、HotSpot | 静态单赋值,优化友好 |
| Sea of Nodes | V8 TurboFan、Graal | 图表示,灵活 |
2.3.3 SSA 形式的 IR
SSA(Static Single Assignment,静态单赋值)是一种 IR 形式,每个变量只被赋值一次。
// 原始代码
int x = 1;
x = x + 2;
x = x * 3;
// SSA 形式
int x1 = 1;
int x2 = x1 + 2;
int x3 = x2 * 3;
// 含分支的 SSA (使用 φ 函数)
// 原始代码
int x = 1;
if (cond) {
x = 2;
}
print(x);
// SSA 形式
int x1 = 1;
if (cond) {
int x2 = 2;
// ...
}
int x3 = φ(x1, x2); // φ 函数选择来自不同路径的值
print(x3);
2.3.4 Graal IR 示例(Sea of Nodes)
// Java 源码
public int compute(int x) {
int a = x * 2;
int b = a + 1;
return b;
}
// Graal IR (Sea of Nodes) 概念表示
// 每个节点代表一个操作,边代表数据/控制依赖
// Node 1: Parameter(0) [x]
// Node 2: Constant(2)
// Node 3: Mul(Node 1, Node 2) [a]
// Node 4: Constant(1)
// Node 5: Add(Node 3, Node 4) [b]
// Node 6: Return(Node 5)
2.3.5 自定义 SSA IR
// Rust 实现简单 SSA IR
#[derive(Debug, Clone)]
enum Value {
Const(i64),
Param(usize),
BinaryOp {
op: BinaryOp,
left: Box<Value>,
right: Box<Value>,
},
}
#[derive(Debug, Clone)]
enum BinaryOp {
Add,
Sub,
Mul,
Div,
}
#[derive(Debug)]
struct Instruction {
result: Variable,
value: Value,
}
#[derive(Debug)]
struct BasicBlock {
label: String,
instructions: Vec<Instruction>,
terminator: Terminator,
}
#[derive(Debug)]
enum Terminator {
Return(Value),
Branch {
condition: Value,
true_target: String,
false_target: String,
},
Jump(String),
}
// 使用示例
fn build_ssa_example() -> Vec<BasicBlock> {
// 计算 fib(n)
// entry:
// n_1 = Param(0)
// cmp = n_1 <= 1
// Branch(cmp, base_case, recursive)
// base_case:
// Return(n_1)
// recursive:
// n_2 = n_1 - 1
// call1 = Call(fib, n_2)
// n_3 = n_1 - 2
// call2 = Call(fib, n_3)
// result = call1 + call2
// Return(result)
todo!()
}
2.4 编译管线
2.4.1 典型编译管线阶段
┌──────────────────────────────────────────────────────────┐
│ JIT 编译管线 │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. 解析 (Parsing) │
│ └─ 源码 → AST │
│ │
│ 2. 字节码生成 (Bytecode Generation) │
│ └─ AST → 字节码 │
│ │
│ 3. IR 生成 │
│ └─ 字节码 → SSA IR │
│ │
│ 4. 优化遍 (Optimization Passes) │
│ ├─ 常量折叠 (Constant Folding) │
│ ├─ 死代码消除 (Dead Code Elimination) │
│ ├─ 公共子表达式消除 (CSE) │
│ ├─ 内联 (Inlining) │
│ ├─ 循环优化 (Loop Optimization) │
│ ├─ 逃逸分析 (Escape Analysis) │
│ └─ 寄存器分配 (Register Allocation) │
│ │
│ 5. 代码生成 (Code Generation) │
│ └─ 优化 IR → 机器码 │
│ │
│ 6. 代码安装 (Code Installation) │
│ └─ 替换解释器入口 │
│ │
└──────────────────────────────────────────────────────────┘
2.4.2 核心优化技术
常量折叠(Constant Folding)
// 优化前
int x = 3 + 4; // 编译时可计算
int y = x * 2; // x 已知为常量
// 优化后
int y = 14; // 直接计算结果
死代码消除(Dead Code Elimination)
// 优化前
int compute(int x) {
int a = x * 2; // a 被使用
int b = x * 3; // b 未被使用
int c = a + 1; // c 被使用
return c;
}
// 优化后
int compute(int x) {
int a = x * 2;
return a + 1;
}
公共子表达式消除(CSE)
// 优化前
int a = x * y + z;
int b = x * y - z;
// 优化后
int temp = x * y;
int a = temp + z;
int b = temp - z;
内联(Inlining)
// 优化前
public int square(int x) {
return multiply(x, x);
}
public int multiply(int a, int b) {
return a * b;
}
// 优化后 (multiply 被内联)
public int square(int x) {
return x * x;
}
2.4.3 Graal 编译管线示例
// 使用 GraalVM 的 Ideal Graph Visualizer 查看 IR
// 启动参数: -Dgraal.Dump=:1
public class GraalPipelineDemo {
public static int compute(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i * 2;
}
return sum;
}
public static void main(String[] args) {
// 预热以触发 JIT 编译
for (int i = 0; i < 100_000; i++) {
compute(1000);
}
}
}
// 编译管线会执行以下主要优化:
// 1. 构建 Sea of Nodes IR
// 2. 高阶图嵌入 (High-tier Graph Embedding)
// 3. 框架图 (Framework Graph)
// 4. 中阶优化 (Mid-tier Optimizations)
// - Partial Escape Analysis
// - Loop Predication
// 5. 低阶优化 (Low-tier Optimizations)
// - Register Allocation
// - Instruction Selection
// 6. 机器码生成
2.5 热点代码检测
2.5.1 热点检测策略
| 策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 计数器 | 统计方法调用次数 | 简单、低开销 | 难以区分循环热点 |
| 回边计数器 | 统计循环回边执行次数 | 精准检测循环 | 只能检测循环 |
| 采样 | 定期采样 PC | 开销极低 | 不够精确 |
| Trace | 记录执行轨迹 | 捕获跨函数热点 | 实现复杂 |
2.5.2 HotSpot 的热点检测
// HotSpot 使用两种计数器
// 1. 方法调用计数器 (Invocation Counter)
// 2. 回边计数器 (Back Edge Counter)
// -XX:CompileThreshold=10000 方法调用阈值
// -XX:OnStackReplacePercentage=140 OSR 阈值百分比
// 热点检测示例
public class HotspotDetectionDemo {
// 方法级热点:被调用多次
public void frequentlyCalled() {
// 这个方法被调用超过 CompileThreshold 次后触发 JIT
}
// 循环级热点:循环执行多次
public void loopIntensive() {
for (int i = 0; i < 1_000_000; i++) {
// 回边计数器增长,触发 OSR 编译
}
}
public static void main(String[] args) {
HotspotDetectionDemo demo = new HotspotDetectionDemo();
// 触发方法级热点
for (int i = 0; i < 20_000; i++) {
demo.frequentlyCalled();
}
// 触发循环级热点
demo.loopIntensive();
}
}
# 查看热点检测
java -XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+LogCompilation \
HotspotDetectionDemo
# 日志输出示例:
# 1234 42 3 HotspotDetectionDemo::frequentlyCalled (4 bytes)
# 编译层次 3 = C1 编译,带完整 Profile
2.5.3 自定义热点检测器
# Python 实现简单热点检测器
import time
from typing import Dict, Callable
from functools import wraps
class HotspotDetector:
"""基于计数器的热点检测器"""
def __init__(self, threshold: int = 1000):
self.threshold = threshold
self.counters: Dict[str, int] = {}
self.hot_functions: set = set()
def track(self, func: Callable) -> Callable:
"""装饰器:跟踪函数调用次数"""
name = func.__qualname__
self.counters[name] = 0
@wraps(func)
def wrapper(*args, **kwargs):
self.counters[name] += 1
# 检查是否达到阈值
if (self.counters[name] >= self.threshold and
name not in self.hot_functions):
self.hot_functions.add(name)
self._on_hotspot_detected(name)
return func(*args, **kwargs)
return wrapper
def _on_hotspot_detected(self, name: str):
"""热点回调"""
print(f"[Hotspot] {name} 已被标记为热点 (调用次数: {self.counters[name]})")
# 在这里可以触发 JIT 编译
def get_stats(self) -> dict:
"""获取统计信息"""
return {
'total_functions': len(self.counters),
'hot_functions': len(self.hot_functions),
'counters': dict(self.counters),
}
# 使用示例
detector = HotspotDetector(threshold=500)
@detector.track
def compute_sum(n: int) -> int:
return sum(range(n))
@detector.track
def process_data(data: list) -> list:
return [x * 2 for x in data]
# 模拟热点函数
for i in range(1000):
compute_sum(100)
process_data([1, 2, 3])
print(detector.get_stats())
2.6 去优化(Deoptimization)
2.6.1 什么是去优化
去优化(Deoptimization)是 JIT 编译的逆过程,当优化后的代码假设不再成立时,回退到解释执行。
正常流程:
字节码 → [JIT编译] → 优化机器码 → 执行
去优化流程:
优化机器码 → [假设失败] → 恢复解释器状态 → 解释执行 → [可能重新编译]
2.6.2 触发去优化的常见原因
| 原因 | 说明 | 示例 |
|---|---|---|
| 类型假设失败 | 优化假设的类型改变 | int 变为 string |
| 分支预测失败 | 未预测到的分支执行 | 罕见的异常路径 |
| 类层次变化 | 加载新类破坏假设 | 新子类加载 |
| 栈上替换失败 | OSR 编译假设不成立 | 循环条件改变 |
2.6.3 V8 去优化示例
// V8 去优化详细示例
// 场景1:类型改变
function add(a, b) {
return a + b;
}
// 初始调用:整数
for (let i = 0; i < 10000; i++) {
add(i, i); // JIT 编译为整数加法
}
// 突然传入字符串
add("hello", "world"); // 去优化!
// 场景2:对象形状改变
function getX(obj) {
return obj.x;
}
// 对象形状一致
let obj1 = { x: 1, y: 2 };
let obj2 = { x: 3, y: 4 };
for (let i = 0; i < 10000; i++) {
getX(obj1);
getX(obj2);
}
// 形状改变
let obj3 = { x: 5, z: 6 }; // 不同的属性布局
getX(obj3); // 可能触发去优化
# 查看 V8 去优化日志
node --trace-opt --trace-deopt demo.js
# 输出示例:
# [marking 0x... for optimization to TURBOFAN]
# [compiling method 0x... using TurboFan]
# [deoptimizing (DEOPT eager): begin 0x... (opt #1)]
# ;; deopt reason: wrong map
# ;; deopt location: demo.js:3
2.6.4 Java HotSpot 去优化
// Java 去优化示例
public class DeoptDemo {
// 初始假设:obj 总是 A 类型
public int process(Object obj) {
if (obj instanceof A) {
return ((A) obj).getValue();
}
return 0;
}
static class A {
int getValue() { return 1; }
}
static class B extends A {
@Override
int getValue() { return 2; }
}
public static void main(String[] args) {
DeoptDemo demo = new DeoptDemo();
A a = new A();
// 预热:只使用 A 类型
for (int i = 0; i < 100_000; i++) {
demo.process(a);
}
// JIT 假设:obj 总是 A 类型
// 类型检查被优化掉
// 加载 B 类后,假设可能失效
// 使用 B 类型
B b = new B();
demo.process(b); // 可能触发去优化
}
}
// 查看去优化
// java -XX:+PrintCompilation -XX:+TraceDeoptimization DeoptDemo
2.6.5 减少去优化的最佳实践
// JavaScript - 保持类型一致性
// ❌ 不好的做法:类型不一致
function process(value) {
if (typeof value === 'number') {
return value * 2;
}
return value.toUpperCase(); // 字符串分支
}
// 交替调用不同类型
process(42);
process("hello");
process(100);
process("world");
// ✅ 好的做法:分离类型
function processNumber(value) {
return value * 2;
}
function processString(value) {
return value.toUpperCase();
}
// Java - 避免类层次假设被破坏
// ❌ 不好的做法
// JIT 假设只有 A 类型
public void process(A obj) {
obj.doSomething();
}
// 后期加载的子类可能破坏假设
// ✅ 好的做法:使用 final 或 sealed
public final class A {
public void doSomething() { }
}
// 或使用 sealed class (Java 17+)
public sealed class Shape permits Circle, Rectangle {
public double area() { return 0; }
}
2.7 内联缓存(Inline Cache)
2.7.1 内联缓存原理
内联缓存是一种加速动态语言属性访问的技术,在调用点缓存类型信息。
第一次调用:
obj.x → 查找属性 x (慢速路径)
缓存: obj 的形状(Shape) → x 的偏移量
后续调用:
obj.x → 检查形状是否匹配
如果匹配 → 直接访问偏移量 (快速路径)
如果不匹配 → 重新查找 (慢速路径)
2.7.2 V8 内联缓存状态
┌─────────────────────────────────────────────────────────┐
│ V8 内联缓存 (Inline Cache) 状态 │
├─────────────────────────────────────────────────────────┤
│ │
│ UNINITIALIZED → 初始状态,未执行过 │
│ │ │
│ ▼ │
│ MONOMORPHIC → 只见过一种形状(最快) │
│ │ │
│ ▼ (见到第二种形状) │
│ POLYMORPHIC → 见过2-4种形状(较快) │
│ │ │
│ ▼ (见到超过4种形状) │
│ MEGAMORPHIC → 退化为通用路径(最慢) │
│ │
└─────────────────────────────────────────────────────────┘
2.7.3 内联缓存实现示例
// 简化版内联缓存实现
typedef struct {
Shape* cached_shape; // 缓存的对象形状
int property_offset; // 属性在对象中的偏移
} InlineCache;
typedef struct {
InlineCache ic;
// ... 其他字段
} PropertyAccessSite;
// 属性访问
Value ic_load_property(PropertyAccessSite* site, Object* obj) {
// 快速路径:形状匹配
if (obj->shape == site->ic.cached_shape) {
return obj->properties[site->ic.property_offset];
}
// 慢速路径:查找属性
int offset = lookup_property(obj, "propertyName");
// 更新缓存
site->ic.cached_shape = obj->shape;
site->ic.property_offset = offset;
return obj->properties[offset];
}
2.8 逃逸分析
2.8.1 什么是逃逸分析
逃逸分析(Escape Analysis)判断对象是否逃逸出当前方法或线程,指导优化决策。
// 逃逸分析示例
public class EscapeAnalysisDemo {
// 对象不逃逸:可以标量替换
public int compute() {
Point p = new Point(1, 2); // p 不逃逸
return p.x + p.y;
}
// 对象逃逸:无法优化
private Point lastPoint;
public void save(int x, int y) {
lastPoint = new Point(x, y); // 逃逸到实例字段
}
// 部分逃逸:可以进行部分优化
public int partialEscape(boolean flag) {
Point p = new Point(1, 2);
if (flag) {
return p.x; // 只在部分路径使用
}
return 0; // 另一条路径不使用 p
}
}
2.8.2 逃逸分析的优化效果
| 优化 | 条件 | 效果 |
|---|---|---|
| 标量替换 | 对象不逃逸 | 用基本类型替代对象分配 |
| 栈上分配 | 对象不逃逸出线程 | 在栈上分配而非堆 |
| 锁消除 | 对象不逃逸出线程 | 移除同步操作 |
| 部分逃逸 | 对象仅在部分路径使用 | 条件分配 |
// 优化前
public int sum() {
Point p = new Point(3, 4);
return p.x + p.y;
}
// 优化后 (标量替换)
public int sum() {
int x = 3;
int y = 4;
return x + y; // 无对象分配
}
2.9 循环优化
2.9.1 常见循环优化技术
public class LoopOptimizations {
// 1. 循环不变量外提 (Loop-Invariant Code Motion)
public void hoist(int[] arr, int x) {
// 优化前
for (int i = 0; i < arr.length; i++) {
arr[i] = x * 10; // x * 10 是循环不变量
}
// 优化后
int temp = x * 10;
for (int i = 0; i < arr.length; i++) {
arr[i] = temp;
}
}
// 2. 循环展开 (Loop Unrolling)
public void unroll(int[] arr) {
// 优化前
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2;
}
// 优化后 (4x展开)
int i = 0;
for (; i + 3 < arr.length; i += 4) {
arr[i] = arr[i] * 2;
arr[i+1] = arr[i+1] * 2;
arr[i+2] = arr[i+2] * 2;
arr[i+3] = arr[i+3] * 2;
}
for (; i < arr.length; i++) {
arr[i] = arr[i] * 2;
}
}
// 3. 循环向量化 (Vectorization)
public void vectorize(int[] a, int[] b, int[] c) {
// 利用 SIMD 指令
for (int i = 0; i < a.length; i++) {
c[i] = a[i] + b[i];
}
}
}
2.10 本章小结
核心概念
┌─────────────────────────────────────────────────────────┐
│ JIT 工作原理要点 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 字节码是 JIT 的起点 │
│ └─ 紧凑、跨平台、易于解释执行 │
│ │
│ 2. IR 是优化的核心 │
│ └─ SSA 形式便于分析和优化 │
│ │
│ 3. 热点检测触发编译 │
│ └─ 计数器、采样、Trace │
│ │
│ 4. 多趟优化提升性能 │
│ └─ 常量折叠、内联、逃逸分析、向量化 │
│ │
│ 5. 去优化保证正确性 │
│ └─ 类型假设失败时回退到解释执行 │
│ │
│ 6. 内联缓存加速动态语言 │
│ └─ 缓存类型信息,避免重复查找 │
│ │
└─────────────────────────────────────────────────────────┘
2.11 扩展阅读
推荐资源
- “Compiler Design in C” by Allen Holub - 编译器设计经典
- “Engineering a Compiler” by Cooper & Torczon - 现代编译器工程
- LLVM Tutorial - 实现简单 JIT 编译器
- GraalVM Documentation - 深入理解现代 JIT 编译器
在线工具
- Compiler Explorer - 在线查看编译器输出
- LLVM Kaleidoscope - LLVM JIT 教程
- GraalVM Visualizer - IR 可视化
上一章: 第1章 - JIT 编译概述 下一章: 第3章 - LuaJIT