JVM即时编译深度解析:C1/C2、分层编译、OSR与日志分析
即时编译(JIT)是JVM性能的核心,它将热点字节码编译为本地机器码,实现接近C++的执行速度。本文深入剖析JIT编译机制,帮助开发者理解并优化代码执行性能。
一、C1与C2编译器对比
1.1 C1编译器(Client Compiler)
定位:快速编译,优化启动性能
核心特点:
- 编译速度快:毫秒级完成编译,牺牲极致性能换取快速响应
- 优化策略简单:基础方法内联、常量折叠、局部变量优化
- 无Profiling:不收集方法执行统计信息(调用次数、分支跳转)
- 适用场景:GUI应用、短期运行程序、对启动时间敏感的场景
技术细节:
- 处理简单方法(Getter/Setter、短方法),编译后代码体积较小
- JDK 8之前可通过
-client参数指定,JDK 8+该参数保留但无效(为兼容性)
代码示例:
java
// C1擅长优化的简单方法
public String getName() {
return this.name; // 方法内联后几乎无开销
}
1.2 C2编译器(Server Compiler)
定位:深度优化,追求峰值性能
核心特点:
- 编译速度慢:需数十毫秒到数百毫秒,进行激进优化
- 全局优化 :基于性能监控数据(Profiling)的优化
- 逃逸分析(栈上分配、标量替换)
- 循环展开(Loop Unrolling)
- 分支预测(Profile-Guided Optimization)
- 公共子表达式消除
- 死代码消除
- 适用场景:长期运行的服务端应用、计算密集型任务
技术细节:
- 编译队列(Compile Queue)机制,后台线程异步编译
- JDK 8之前可通过
-server参数指定,JDK 8+默认启用
1.3 Graal JIT编译器(JDK 10+引入)
定位:C2的替代者,支持AOT编译
特点:
- 用Java重写(C1/C2用C++),易于维护与扩展
- 支持提前编译(AOT)生成原生镜像(GraalVM Native Image)
- 性能与C2相当,某些场景更优
二、分层编译(Tiered Compilation)
2.1 核心思想
JDK 7+引入,结合C1的快速编译和C2的深度优化,形成5级编译体系。
优势:
- 启动快:初期用C1快速编译,应用快速响应
- 峰值性能高:热点代码逐步升级到C2优化
- 精准优化:基于Profiling数据做针对性优化
2.2 五级编译层次详解
| 层级 | 类型 | 核心职责 | 优化程度 | 编译耗时 | 触发阈值 |
|---|---|---|---|---|---|
| Level 0 | 解释执行 | 快速启动,收集基础数据 | 无 | 0 | 方法被调用 |
| Level 1 | C1编译(无Profiling) | 轻量编译,基础性能 | 低 | 毫秒级 | 方法调用≥1500次 |
| Level 2 | C1编译(受限Profiling) | C2队列满时快速编译 | 中 | 低 | C2队列繁忙 |
| Level 3 | C1编译(完全Profiling) | 收集完整数据供C2优化 | 高 | 中等 | 方法调用≥15000次 |
| Level 4 | C2编译 | 激进优化,峰值性能 | 极高 | 百毫秒级 | 方法调用≥10000次且Profiling充分 |
注 :阈值可通过-XX:TierXCompileThreshold调整
Level 0:解释执行
- 所有方法初始状态
- 热点探测 :收集方法调用计数器 和循环回边计数器
- 示例:
java
public static void main(String[] args) {
for (int i = 0; i < 30000; i++) {
process(i); // 调用30000次,触发编译升级
}
}
Level 1:C1简单编译
- 适用方法:简单Getter/Setter、无循环/分支的方法
- 优化:基础内联、常量折叠
- 特点 :不插入Profiling代码,代码体积小,执行快
Level 2:C1受限编译
- 触发条件 :C2编译队列已满
- 目标:快速编译以提高性能,避免方法长时间解释执行
- 后续会重新编译为Level 3/4
Level 3:C1完全Profiling
- 插入Profiling:记录方法调用次数、分支跳转频率、类型继承关系
- 数据用途 :为C2提供精确优化依据
- 开销:Profiling代码有一定性能损耗
Level 4:C2激进优化
- 优化策略 :
- 方法内联:基于调用频率内联热点方法
- 去虚拟化 :根据Profiling发现
a.b()总是调用ClassA.b(),消除虚方法调用 - 分支预测:优先编译热点分支
- 逃逸分析:栈上分配对象,减少GC压力
- 去优化(Deoptimization):若Profiling假设失效(如内联的类型变更),退回到Level 0重新收集
2.3 分层编译协作流程
方法调用 → Level 0解释 → 计数达标 → Level 1 C1编译 → Level 3 C1+Profiling → Level 4 C2优化
↓
C2队列满 → Level 2 C1受限编译
↓
假设失效 → Deoptimization → 回到Level 0
关键机制:
- 逆优化(Deoptimization) :当C2的激进优化假设被打破(如内联的类型被替换),JVM会退回到解释执行,重新收集Profiling数据后再次编译
- 代码缓存 :编译后的机器码存储在Code Cache (默认240MB,可通过
-XX:ReservedCodeCacheSize调整)
三、栈上替换(OSR - On-Stack Replacement)
3.1 OSR定义
在方法执行过程中 替换其正在执行的栈帧,主要解决长循环的优化问题。
场景 :main方法执行很长时间,内部循环体是热点,但方法本身调用次数不足,无法触发编译。
3.2 OSR触发机制
java
public static void main(String[] args) {
long sum = 0;
for (int i = 0; i < 1_000_000_000; i++) {
sum += i; // 循环回边计数器递增
// i达到阈值后,触发OSR编译
}
}
- 循环回边计数器 :每次循环末尾
i++触发 - 阈值 :
-XX:OnStackReplacePercentage=140(默认值,基于CompileThreshold计算)
3.3 OSR编译日志特征
1234 567 % 3 com.example.Main::main @ 10 (58 bytes)
# %表示OSR
# @ 10 表示从字节码偏移量10处开始OSR
3.4 OSR vs 正常编译
- 正常编译 :替换整个方法的入口
- OSR :替换方法内部特定位置的栈帧,保留当前执行上下文
四、PrintCompilation日志分析实战
4.1 开启编译日志
bash
# 基础日志(时间戳、编译ID、层级、方法名)
-XX:+PrintCompilation
# 详细日志(包含字节码大小、属性标记)
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
4.2 日志格式解析
基础格式:
时间戳 编译ID 属性 层级 类名::方法名 (字节码大小)
1234 567 % 3 com.example.Main::main @ 10 (58 bytes)
字段详解:
| 字段 | 示例 | 含义 |
|---|---|---|
| 时间戳 | 1234 |
JVM启动后的毫秒数 |
| 编译ID | 567 |
自增ID,唯一标识一次编译任务 |
| 属性 | % |
% OSR, s synchronized, ! 含异常处理, b 阻塞模式 |
| 层级 | 3 |
0-4的编译级别 |
| 方法名 | com.example.Main::main |
类名::方法名 |
| 字节码大小 | (58 bytes) |
方法字节码长度 |
4.3 实战日志分析
示例代码:
java
public class TieredCompilation {
public static void main(String[] args) {
for (int i = 0; i < 30000; i++) {
process(i);
}
}
private static void process(int value) {
// 模拟业务逻辑
int sum = value * 2 + value;
System.out.println(sum);
}
}
编译日志输出:
1023 788 1 com.example.Article::getName (5 bytes) ← Level 1编译
1025 789 1 com.example.Article::getAuthor (5 bytes)
1032 800 3 com.example.JsonFormatter::<init> (5 bytes) ← Level 3编译
1032 801 3 com.example.Article::<init> (15 bytes)
1041 820 3 com.example.JsonFormatter::format (8 bytes)
1122 903 4 com.example.JsonFormatter::<init> (5 bytes) ← Level 4编译
1123 800 3 com.example.JsonFormatter::<init> (5 bytes) made not entrant ← 旧版本失效
1123 904 4 com.example.Article::<init> (15 bytes)
1124 801 3 com.example.Article::<init> (15 bytes) made not entrant
1132 932 % 3 com.example.TieredCompilation::main @ 2 (58 bytes) ← OSR编译
1133 933 3 com.example.TieredCompilation::main (58 bytes)
1144 940 % 4 com.example.TieredCompilation::main @ 2 (58 bytes) ← OSR升级到Level 4
1145 932 % 3 com.example.TieredCompilation::main @ 2 (58 bytes) made not entrant
日志解读:
- Level 1 :
getName/getAuthor简单方法,编译后代码体积5字节 - Level 3 :构造函数
<init>和业务方法format,开始Profiling - Level 4 :C2重新编译
<init>,Level 3版本标记为made not entrant(不可进入) - OSR编译 :
main方法因循环次数多触发OSR,先Level 3后升级到Level 4
made not entrant含义 :旧版本编译代码已被废弃,但可能仍有线程在执行,待执行完成后彻底回收
4.4 关键日志场景
场景1:Deoptimization(逆优化)
1234 567 4 com.example.Service::process (100 bytes)
2345 567 4 com.example.Service::process (100 bytes) made not entrant
- 第二行表示假设失效,C2优化被撤销,退回到解释执行
场景2:编译失败
1234 567 ! 3 com.example.Service::process (100 bytes)
# ! 表示方法有异常处理器,可能影响优化
4.5 性能分析
高频编译问题:若某方法反复编译(Level 3→4→3→4),说明Deoptimization严重,需检查代码:
- 类型不稳定:方法入参实际类型多变
- 分支预测失败:热点分支数据不收敛
编译耗时估算:
- Level 1:<1ms
- Level 3:5-20ms
- Level 4:50-500ms(取决于方法复杂度)
优化建议 :避免在应用启动初期调用复杂方法(如main中初始化),否则会导致启动慢,因为C2编译阻塞执行。
五、编译器调优参数
5.1 分层编译控制
bash
# 禁用分层编译(不推荐,除非调试)
-XX:-TieredCompilation
# 强制C2编译阈值(默认10000)
-XX:Tier4CompileThreshold=5000 # 更早触发C2
# 强制C1编译阈值(默认1500)
-XX:Tier3CompileThreshold=1000
# OSR阈值调整
-XX:OnStackReplacePercentage=140 # 默认140
-XX:CompileThreshold=10000
# OSR触发 = CompileThreshold * (OnStackReplacePercentage / 100)
5.2 Code Cache调优
bash
# Code Cache默认240MB(32位JVM 48MB)
-XX:ReservedCodeCacheSize=512m # 大规模应用需增大
-XX:InitialCodeCacheSize=64m
# 监控Code Cache使用率
jcmd <pid> Compiler.codecache
Code Cache溢出后果:无法编译新热点方法,性能回退到解释执行
5.3 编译线程调优
bash
# C2编译线程数(默认CPU核数)
-XX:CICompilerCount=4
# 编译队列大小
-XX:CompilerThreadPriority=10
线程拥堵表现:热点方法长时间停留在解释执行,吞吐量下降
六、最佳实践与避坑
6.1 启动性能优化
bash
# 激进编译阈值,加快启动
-XX:Tier4CompileThreshold=5000
# 但可能导致过早编译,Profiling数据不足
推荐:保持默认值,除非压测证明有益
6.2 避免去优化
java
// 错误:类型不稳定导致Deopt
public void process(Object obj) {
if (obj instanceof String) {
// 90%情况走这里
} else if (obj instanceof Integer) {
// 10%情况
}
}
// C2内联String分支后,突然传入Integer,触发Deopt
// 优化:接口隔离
public void processString(String s) {}
public void processInteger(Integer i) {}
6.3 分析热点代码
bash
# JITWatch工具(基于PrintCompilation日志)
java -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:LogFile=/tmp/jit.log
# 配合JITWatch GUI查看
6.4 编译诊断
bash
# 查看编译队列
jcmd <pid> Compiler.queue
# 查看已编译方法
jcmd <pid> Compiler.codelist
# 查看编译器线程
jstack <pid> | grep "C1/C2 CompilerThread"
总结
核心要点
- C1 vs C2:快速启动 vs 峰值性能
- 分层编译:0→1→3→4的渐进优化,自动平衡
- OSR :解决长循环优化问题,
%标记识别 - 日志分析:时间戳、层级、made not entrant是关键
调优口诀
启动慢降阈值,性能差升层级
Deopt要避免,类型需稳定
Cache别溢出,线程莫拥堵
掌握JIT编译机制,能让你在性能调优时有的放矢,编写出更JVM友好的代码。