JVM 即时编译深度解析:C1/C2、分层编译、OSR 与日志分析
即时编译(JIT)是 JVM 性能的核心,它将热点字节码编译为本地机器码,实现接近 C++ 的执行速度。本文深入剖析 JIT 编译机制,帮助开发者理解并优化代码执行性能。
一、C1 与 C2 编译器对比
1.1 C1 编译器(Client Compiler)
定位:快速编译,优化启动性能
核心特点:
- 编译速度快:毫秒级完成编译,牺牲极致性能换取快速响应
- :基础方法内联、常量折叠、局部变量优化
JVM 即时编译机制通过 C1 快速启动与 C2 深度优化结合分层编译策略。Level 0 至 Level 4 逐步提升优化程度,OSR 解决长循环热点问题。日志分析可识别编译层级、去优化及性能瓶颈。调优参数涉及编译阈值、Code Cache 及线程数,避免类型不稳定导致的逆优化,确保应用启动快且峰值性能高。
即时编译(JIT)是 JVM 性能的核心,它将热点字节码编译为本地机器码,实现接近 C++ 的执行速度。本文深入剖析 JIT 编译机制,帮助开发者理解并优化代码执行性能。
定位:快速编译,优化启动性能
核心特点:
技术细节:
-client 参数指定,JDK 8+ 该参数保留但无效(为兼容性)代码示例:
// C1 擅长优化的简单方法
public String getName() {
return this.name; // 方法内联后几乎无开销
}
定位:深度优化,追求峰值性能
核心特点:
技术细节:
-server 参数指定,JDK 8+ 默认启用定位:C2 的替代者,支持 AOT 编译
特点:
JDK 7+ 引入,结合 C1 的快速编译和 C2 的深度优化,形成 5 级编译体系。
优势:
| 层级 | 类型 | 核心职责 | 优化程度 | 编译耗时 | 触发阈值 |
|---|---|---|---|---|---|
| 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 调整
public static void main(String[] args) {
for (int i = 0; i < 30000; i++) {
process(i); // 调用 30000 次,触发编译升级
}
}
a.b() 总是调用 ClassA.b(),消除虚方法调用方法调用 → Level 0 解释 → 计数达标 → Level 1 C1 编译 → Level 3 C1+Profiling → Level 4 C2 优化 ↓ C2 队列满 → Level 2 C1 受限编译 ↓ 假设失效 → Deoptimization → 回到 Level 0
关键机制:
-XX:ReservedCodeCacheSize 调整)在方法执行过程中替换其正在执行的栈帧,主要解决长循环的优化问题。
场景:main 方法执行很长时间,内部循环体是热点,但方法本身调用次数不足,无法触发编译。
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 计算)1234 567 % 3 com.example.Main::main @ 10 (58 bytes) # %表示 OSR # @ 10 表示从字节码偏移量 10 处开始 OSR
# 基础日志(时间戳、编译 ID、层级、方法名)
-XX:+PrintCompilation
# 详细日志(包含字节码大小、属性标记)
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining
基础格式:
时间戳 编译 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) | 方法字节码长度 |
示例代码:
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
日志解读:
getName/getAuthor 简单方法,编译后代码体积 5 字节<init> 和业务方法 format,开始 Profiling<init>,Level 3 版本标记为 made not entrant(不可进入)main 方法因循环次数多触发 OSR,先 Level 3 后升级到 Level 4made not entrant 含义:旧版本编译代码已被废弃,但可能仍有线程在执行,待执行完成后彻底回收
场景 1:Deoptimization(逆优化)
1234 567 4 com.example.Service::process (100 bytes)
2345 567 4 com.example.Service::process (100 bytes) made not entrant
场景 2:编译失败
1234 567 ! 3 com.example.Service::process (100 bytes) # ! 表示方法有异常处理器,可能影响优化
高频编译问题:若某方法反复编译(Level 3→4→3→4),说明 Deoptimization 严重,需检查代码:
编译耗时估算:
优化建议:避免在应用启动初期调用复杂方法(如 main 中初始化),否则会导致启动慢,因为 C2 编译阻塞执行。
# 禁用分层编译(不推荐,除非调试)
-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)
# Code Cache 默认 240MB(32 位 JVM 48MB)
-XX:ReservedCodeCacheSize=512m # 大规模应用需增大
-XX:InitialCodeCacheSize=64m # 监控 Code Cache 使用率
jcmd <pid> Compiler.codecache
Code Cache 溢出后果:无法编译新热点方法,性能回退到解释执行
# C2 编译线程数(默认 CPU 核数)
-XX:CICompilerCount=4
# 编译队列大小
-XX:CompilerThreadPriority=10
线程拥堵表现:热点方法长时间停留在解释执行,吞吐量下降
# 激进编译阈值,加快启动
-XX:Tier4CompileThreshold=5000
# 但可能导致过早编译,Profiling 数据不足
推荐:保持默认值,除非压测证明有益
// 错误:类型不稳定导致 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) {}
# JITWatch 工具(基于 PrintCompilation 日志)
java -XX:+UnlockDiagnosticVMOptions
-XX:+TraceClassLoading
-XX:+LogCompilation
-XX:LogFile=/tmp/jit.log # 配合 JITWatch GUI 查看
# 查看编译队列
jcmd <pid> Compiler.queue
# 查看已编译方法
jcmd <pid> Compiler.codelist
# 查看编译器线程
jstack <pid> | grep "C1/C2 CompilerThread"
% 标记识别启动慢降阈值,性能差升层级
Deopt 要避免,类型需稳定
Cache 别溢出,线程莫拥堵
掌握 JIT 编译机制,能让你在性能调优时有的放矢,编写出更 JVM 友好的代码。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online