JIT 编译优化
Java 代码先由解释器逐条执行字节码,运行过程中 JIT(Just-In-Time)编译器把热点代码编译为本地机器码,提升执行效率。现代 JVM 采用 C1 + C2 的分层编译模型,辅以 Graal 等新一代编译器。
解释执行与 JIT 编译
字节码 ──→ 解释器逐条解释(启动快、执行慢)
──→ JIT 编译为机器码(启动慢、执行快)- 解释器:程序启动时立即运行,无需编译等待
- JIT 编译器:统计热点(方法调用次数、循环回边次数),达到阈值后编译为机器码并缓存
两者配合,兼顾启动速度与长期执行性能。热点判定由计数器驱动:
| 计数器 | 作用 |
|---|---|
| 方法调用计数器 | 统计方法被调用的次数 |
| 回边计数器 | 统计方法内循环执行的次数 |
达到阈值(默认方法调用 10000 次,可经 -XX:CompileThreshold 调整)后触发编译。
编译器分层:C1 与 C2
C1(客户端编译器)
- 编译速度快,优化深度浅
- 适合需要快速达到编译结果的场景(启动期)
- 用于分层编译的 1-3 层
C2(服务端编译器)
- 编译速度慢,但优化极为激进(依赖分析、逃逸分析、向量化等)
- 适合长时间运行的服务器应用
- 用于分层编译的 4 层
分层编译
JDK 7 引入的分层编译将执行分为 5 层,兼顾启动与稳态性能:
| 层 | 状态 | 说明 |
|---|---|---|
| 0 | 解释执行 | 收集性能信息 |
| 1 | C1 编译(无 profiling) | 快速编译 |
| 2 | C1 编译(部分 profiling) | 轻量 profiling |
| 3 | C1 编译(完全 profiling) | 收集热点分支等 |
| 4 | C2 编译 | 激进优化 |
执行路径通常从 0 层 → 3 层 → 4 层。编译器通过分层获取运行期信息,C2 基于这些信息做更精准的优化。
参数
| 参数 | 作用 |
|---|---|
-XX:+TieredCompilation | 开启分层编译(默认) |
-XX:-TieredCompilation | 关闭分层编译 |
-XX:CompileThreshold | 解释器到 C1 的编译阈值 |
-XX:CICompilerCount | 编译器线程数 |
逃逸分析
逃逸分析(Escape Analysis)分析对象的作用域:对象是否逃逸出方法/线程。
| 逃逸级别 | 含义 |
|---|---|
| 不逃逸 | 对象只在本方法内使用 |
| 方法逃逸 | 对象被传递给其他方法 |
| 线程逃逸 | 对象被其他线程访问 |
对象不逃逸时,JIT 可以做三种优化:
1. 栈上分配(被标量替换取代)
将对象拆分为多个标量(基本类型字段),分配到栈上或寄存器,避免堆分配与 GC:
java
class Point { int x, y; }
double length(Point p) {
Point tmp = new Point(p.x, p.y); // tmp 未逃逸
return Math.sqrt(tmp.x * tmp.x + tmp.y * tmp.y);
}tmp 未逃逸,编译器可将其标量替换为两个局部变量 x、y,无需在堆上创建对象。
2. 标量替换
把对象的字段拆成独立标量,直接使用,无需对象头、无需堆内存。
3. 锁消除
对象未逃逸时,同步锁可以消除(配合锁消除优化)。
方法内联
把被调用方法的字节码直接嵌入调用处,消除方法调用开销(压栈、跳转、返回),是 JIT 最重要的优化之一。
java
// 原始代码
int result = add(1, 2);
// 内联后
int result = 1 + 2; // add 的代码直接展开内联条件
- 方法体足够小(默认
-XX:MaxInlineSize=35字节) - 调用频繁(热点方法)
- 方法不能是虚方法,或可基于类型分析(CHA)确定唯一实现
接口/虚方法通过**类型分析(CHA)**判断是否有多个实现:单实现直接内联;多实现时先内联目标,若运行期类型变化则退化执行(-XX:+UseInlineCaches 内联缓存)。
循环优化
JIT 对循环做多种优化:
| 优化 | 说明 |
|---|---|
| 循环展开 | 将循环体复制多份,减少循环控制开销 |
| 循环剥离 | 把可证明不执行的迭代剥离 |
| 循环不变代码外提 | 循环体内不变的计算提到循环外 |
| 向量化 | 将标量运算向量化为 SIMD 指令(C2 的 SuperWord) |
java
for (int i = 0; i < n; i++) {
sum += arr[i] * CONST; // CONST * arr[i] 可向量化,一次处理多个元素
}Graal JIT 编译器
Graal 是新一代 JIT 编译器,用 Java 编写,面向 HotSpot 与 GraalVM:
| 特性 | 说明 |
|---|---|
| 编译速度 | 比 C2 更快 |
| 优化深度 | 具备 C2 的多数优化,另有部分增强 |
| 部署方式 | GraalVM 内置;OpenJDK 中可用 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用 |
| 编译策略 | 面向"即时编译 + 提前编译"统一设计 |
Graal 与 C2 的差异更多体现在工程实现与维护上,对多数应用,默认 C2 已足够。
常见面试点
- JIT 一定比解释快吗? 稳态性能是,但编译本身有开销与预热时间;短命进程解释执行反而更合适。
- 逃逸分析的效果? 未逃逸对象可标量替换/栈上分配/锁消除,减少堆压力与 GC。
- 为什么 Java 启动慢? 解释器启动快但执行慢,C2 编译需要时间预热;分层编译 + AOT(GraalVM Native Image)可改善。
- 如何观察 JIT 行为?
-XX:+PrintCompilation打印编译日志,jstat -compiler查看编译统计,JFR 的 Compiler 事件可观测。