1. 触发编译
JIT编译的触发基于热点代码检测,主要通过两种计数器:
• 方法调用计数器:统计方法被调用的次数(默认阈值:C1为1,500次,C2为10,000次)。
• 回边计数器:统计循环体的执行次数(如for
、while
中的跳转指令)。
当计数器超过阈值时,JVM将代码加入编译队列,由后台编译线程处理。
2. 编译流程(以HotSpot JVM为例)
JIT的编译过程分为多个阶段,不同编译器(C1/C2/Graal)细节略有差异,但核心流程相似:
(1) 字节码解析与中间表示(IR)生成
• 输入:字节码(.class
文件中的指令)。
• 输出:生成高级中间表示(HIR),如控制流图(CFG)。
• 关键操作:
• 解析字节码,构建基本块(Basic Block)。
• 识别循环结构、异常处理块等。
(2) 方法内联(Method Inlining)
• 优化目标:减少方法调用开销。
• 规则:
• 内联小方法(如getter/setter)。
• 避免内联巨型方法(通过-XX:MaxInlineSize=35
控制阈值)。
• 示例:
// 内联前
int getValue() { return value; }
void print() { System.out.println(getValue()); }// 内联后
void print() { System.out.println(value); }
(3) 逃逸分析(Escape Analysis)
• 目标:判断对象是否仅在方法内部使用。
• 优化结果:
• 栈上分配:对象直接分配在栈帧,减少GC压力。
• 标量替换:将对象拆解为基本类型局部变量。
• 锁消除:若对象无竞争,移除同步操作。
• 示例:
// 优化前
void foo() {Object obj = new Object(); // 对象可能被分配在堆synchronized(obj) { ... } // 同步锁
}// 优化后(逃逸分析确认obj未逃逸)
void foo() {// 对象被标量替换或栈上分配,锁被消除
}
(4) 循环优化
• 循环展开(Loop Unrolling):减少循环条件判断次数。
// 优化前
for (int i = 0; i < 4; i++) { sum += i; }// 优化后
sum += 0; sum += 1; sum += 2; sum += 3;
• 循环向量化(SIMD):利用CPU单指令多数据(如AVX指令)并行计算数组。
(5) 公共子表达式消除(CSE)
• 移除冗余计算:复用相同表达式的计算结果。
// 优化前
int a = x * y + z;
int b = x * y + w;// 优化后
int tmp = x * y;
int a = tmp + z;
int b = tmp + w;
(6) 死代码消除(Dead Code Elimination)
• 移除不可达代码:如if (false) { ... }
或未使用的变量。
// 优化前
int x = 1;
if (false) { x = 2; } // 死代码
System.out.println(x);// 优化后
System.out.println(1);
(7) 本地代码生成
• 目标平台适配:根据CPU架构(x86/ARM)生成机器码。
• 寄存器分配:优化寄存器使用,减少内存访问。
• 指令选择:选择高效指令(如用LEA
替代乘法)。
3. 分层编译(Tiered Compilation)
现代JVM(如HotSpot)采用分层策略逐步优化代码:
- 第0层:解释执行(快速启动)。
- 第1层(C1编译器):轻量优化(方法内联、简单循环展开)。
- 第2层(C2编译器):深度优化(逃逸分析、向量化)。
- Graal编译器(可选):替代C2,支持更激进优化(如JDK17的GraalVM)。
触发条件:
• C1编译:方法调用约1,500次(-XX:CompileThreshold
)。
• C2编译:方法调用约10,000次(-XX:Tier4CompileThreshold
)。
4. 反优化(Deoptimization)
当假设条件不满足时,JVM会回退到解释执行,常见场景:
• 类加载变化:如动态加载新类导致原有优化失效。
• 多态调用:虚方法的目标方法改变(如接口实现类新增)。
• 断言失败:逃逸分析假设被打破(对象意外逃逸)。
5. 监控与调优
(1) 查看JIT编译日志
java -XX:+PrintCompilation -XX:+PrintInlining -XX:+UnlockDiagnosticVMOptions MyApp
输出示例:
42 3 java.lang.String::hashCode (55 bytes) COMPILE SKIPPED: already compiled
56 1 java.util.ArrayList::size (5 bytes) INLINE (hot)
(2) 关键JVM参数
参数 | 作用 |
---|---|
-XX:+TieredCompilation | 启用分层编译(默认开启) |
-XX:CompileThreshold=1500 | 调整C1编译的调用阈值 |
-XX:MaxInlineSize=35 | 控制方法内联的最大字节码大小 |
-XX:+PrintAssembly | 打印生成的机器码(需HSDIS插件) |
6. 实际案例:优化斐波那契数列
原始代码(递归)
long fib(int n) {if (n <= 1) return n;return fib(n-1) + fib(n-2); // 重复计算,性能差
}
JIT优化后
- 内联:递归调用被展开。
- 循环展开:转换为迭代形式。
- 最终机器码:近似以下优化版本:
long fib(int n) {if (n <= 1) return n;long a = 0, b = 1;for (int i = 2; i <= n; i++) {long c = a + b;a = b;b = c;}return b; }
性能提升:从指数级时间复杂度(O(2^n))降至线性(O(n))。
7. 总结
• 触发条件:基于热点代码的调用或循环次数。
• 核心优化:方法内联、逃逸分析、循环优化、死代码消除等。
• 分层编译:逐步从解释执行过渡到深度优化(C1→C2)。
• 反优化:动态适应运行时变化,保证正确性。
• 调优建议:监控编译日志,合理配置阈值,避免冷启动影响性能测试。
理解JIT编译过程有助于编写对编译器友好的代码(如小方法、减少动态绑定),最大化运行时性能。