解释执行与JIT编译
JVM 执行字节码主要依赖解释器和 JIT 编译器。解释器让程序快速启动,JIT 让热点代码跑得更快。
# 1. 解释执行
解释器逐条读取字节码并执行。
优点:
- 启动快。
- 实现简单。
- 不需要等待编译。
缺点:
- 长期运行性能不如本地机器码。
解释执行适合冷代码,也适合程序启动初期。
# 2. JIT 编译
JIT(Just-In-Time)即时编译会把热点字节码编译成本地机器码。
字节码 -> JIT编译 -> 机器码
机器码可以直接被 CPU 执行,性能更高。
# 3. 为什么不全部提前编译
全部提前编译会带来问题:
- 启动变慢。
- 编译不常执行的代码浪费资源。
- 缺少运行时 profiling 信息。
- 很多优化依赖运行时真实类型和分支概率。
JIT 的优势是可以根据运行时数据做优化。
# 4. 分层编译
HotSpot 使用分层编译思想。
常见编译器:
- C1:编译快,优化较少,适合启动阶段。
- C2:编译慢,优化强,适合热点代码。
分层编译让 JVM 在启动速度和峰值性能之间取得平衡。
# 5. 热点探测
JVM 会通过计数器识别热点代码:
- 方法调用计数器。
- 回边计数器。
方法被频繁调用,或者循环执行次数很多,就可能触发 JIT 编译。
# 6. 反优化
JIT 优化基于运行时假设。如果假设后来不成立,JVM 可能撤销已编译代码,回退到解释执行或重新编译。
例如:
- 原本只有一个实现类,后来加载了新的子类。
- 类型分布发生变化。
- 内联假设失效。
这就是 JVM 动态优化的灵活性。
# 7. 阶段小结
解释执行保证启动速度,JIT 编译提升长期性能。现代 JVM 通过分层编译、热点探测和反优化,把运行时信息转化为性能收益。理解 JIT,是理解 Java 为什么能在长期运行后越来越快的关键。
# 8. 分层编译流程图
方法首次执行
│
▼
解释器执行
│
├─ 调用次数少
│ └─ 继续解释执行
│
└─ 达到热点阈值
▼
C1 编译(快,优化少)
│
├─ 热度下降
│ └─ 使用现有编译结果
│
└─ 持续热点
▼
C2 编译(慢,优化强)
│
▼
执行优化后的机器码
解释器、C1、C2 不是互斥关系,而是共同组成分层执行体系。
# 9. 为什么 Java 有预热
Java 服务刚启动时,很多代码还在解释执行或低级别编译阶段。随着请求进入,热点路径被识别并编译优化,性能逐渐稳定。
启动初期:解释执行多,RT 较高
预热阶段:热点逐渐编译,RT 下降
稳定阶段:热点路径机器码执行,RT 稳定
这就是为什么压测 Java 服务时要有预热阶段。没有预热的压测数据会低估长期运行性能。
# 10. 反优化示例
假设接口只有一个实现:
interface PayService {
void pay();
}
class AliPayService implements PayService {
public void pay() {
}
}
JIT 可能把接口调用优化成直接调用。后来动态加载了新的实现:
class WechatPayService implements PayService {
public void pay() {
}
}
原来的单实现假设不成立,JVM 可能反优化,回退后重新收集类型信息。
# 专家速查表
| 执行方式 | 优势 | 代价 |
|---|---|---|
| 解释执行 | 启动快、无需编译等待 | 长期运行性能较低 |
| C1 编译 | 编译快、适合启动阶段 | 优化深度有限 |
| C2 编译 | 优化强、峰值性能高 | 编译成本更高 |
| 分层编译 | 兼顾启动和峰值性能 | 行为更复杂 |
| OSR | 循环中途切到编译代码 | 分析时调用栈更复杂 |
JIT 优化依赖运行时画像。压测预热不足、代码路径变化、分支预测失效,都可能导致线上性能和本地测试差异明显。
# 11. Tips 快问快答
Q:为什么 Java 程序刚启动时慢一点?
A:热点代码还没充分 JIT 编译,类加载和框架初始化也在发生。
Q:JIT 编译后的机器码存在哪里?
A:通常存放在 Code Cache 中。
Q:解释执行是不是没用了?
A:不是。解释器让程序快速启动,也为 JIT 收集 profiling 信息。
Q:为什么微基准测试要用 JMH?
A:JMH 能处理预热、死代码消除、编译优化等问题,普通 main 方法很容易测错。