【JVM深度解析】第19篇:JIT编译器深度解析
摘要JITJust-In-Time编译器是 JVM 性能的核心引擎——它把字节码即时编译成机器码让 Java 的执行速度接近 C。HotSpot 的 JIT 编译器分为 C1客户端编译器快速编译和 C2服务端编译器深度优化两层配合分层编译Tiered Compilation实现启动速度与峰值性能的平衡。本文深入解析 JIT 的工作原理C1/C2 的区别、分层编译的四个阶段、热点检测HitSpot、方法内联Inlining、逃逸分析Escape Analysis、以及如何阅读 JIT 编译日志。掌握 JIT 知识你才能真正理解为什么某些代码越跑越快。引言很多 Java 开发者有一个困惑Java 明明是解释执行的为什么运行速度能和 C 比肩答案是 JIT 编译器。JVM 在运行时会对热点代码Hot Spot进行即时编译将字节码翻译成高效的机器码。这个过程发生在程序运行期间所以叫即时编译。Java 执行模式演进 ┌──────────────────────────────────────────────────────────────────┐ │ │ │ 早期纯解释执行 │ │ 字节码 → 解释器逐条执行 → 慢 │ │ │ │ 现在解释 JIT 编译 │ │ 字节码 → 解释执行启动快 │ │ → 检测热点 → JIT 编译运行久 │ │ → 执行机器码高效 │ │ │ │ 效果启动稍慢但运行后期性能接近原生代码 │ │ │ └──────────────────────────────────────────────────────────────────┘一、HotSpot JIT 架构1.1 C1 和 C2 编译器HotSpot VM 有两个 JIT 编译器┌──────────────────────────────────────────────────────────────────┐ │ HotSpot JIT 编译器架构 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────┐ │ │ │ JVM 启动 │ │ │ └──────────┬───────────┘ │ │ ↓ │ │ ┌──────────────────────┐ │ │ │ TieredCompilation │ │ │ │ 分层编译 │ │ │ └──────────┬───────────┘ │ │ ↓ │ │ ┌────────────────┴────────────────┐ │ │ ↓ ↓ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ C1 编译器 │ │ C2 编译器 │ │ │ │ (Client Compiler)│ │ (Server Compiler)│ │ │ │ │ │ │ │ │ │ 编译快优化浅 │ │ 编译慢优化深 │ │ │ │ 启动阶段使用 │ │ 稳态运行使用 │ │ │ └─────────────────┘ └─────────────────┘ │ │ ↑ ↑ │ │ └────────────────┬────────────────┘ │ │ ↓ │ │ ┌──────────────────────┐ │ │ │ 机器码执行 │ │ │ └──────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘1.2 C1 vs C2 特性对比┌──────────────────────────────────────────────────────────────────┐ │ C1 vs C2 编译器特性对比 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 特性 │ C1 (Client) │ C2 (Server) │ │ ─────────────────┼──────────────────┼─────────────────────── │ │ 编译速度 │ 快毫秒级 │ 慢秒级 │ │ 优化程度 │ 浅基础优化 │ 深激进优化 │ │ 生成代码质量 │ 一般 │ 高 │ │ 适用场景 │ 启动阶段/短期运行 │ 长期运行/峰值性能 │ │ 主要优化 │ 内联、逃逸分析 │ 寄存器分配、循环展开 │ │ 编译阈值 │ 1500 次调用 │ 10000 次调用 │ │ │ └──────────────────────────────────────────────────────────────────┘二、分层编译详解2.1 四个编译层级分层编译Tiered Compilation将代码编译分为四个阶段┌──────────────────────────────────────────────────────────────────┐ │ 分层编译阶段 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Tier 0: 解释执行 │ │ ├─ 字节码直接由解释器执行 │ │ └─ 收集热点方法信息 │ │ ↓ │ │ Tier 1: C1 编译简单优化 │ │ ├─ 快速编译触发条件热点检测 │ │ ├─ 编译阈值约 1500 次调用 │ │ └─ 代码质量中等 │ │ ↓ │ │ Tier 2: C1 编译完整优化 │ │ ├─ 增加栈帧替换OSR支持 │ │ └─ 代码质量较好 │ │ ↓ │ │ Tier 3: C1 编译最高优化 │ │ └─ 激进优化 │ │ ↓ │ │ Tier 4: C2 编译最终优化 │ │ ├─ 深度优化寄存器分配、循环展开 │ │ ├─ 编译阈值约 10000 次调用 │ │ └─ 代码质量最高 │ │ │ └──────────────────────────────────────────────────────────────────┘2.2 热点检测Hit CounterJIT 通过两种计数器检测热点// 方法调用计数器publicclassCounterDemo{publicvoidhotMethod(){// 每调用一次方法调用计数器 1// 达到阈值 → 触发 JIT 编译}}// 回边计数器循环计数器publicclassLoopDemo{publicvoidhotLoop(){// for 循环每执行一次回边计数器 1// 达到阈值 → 触发 OSROn-Stack Replacement}}// JVM 参数控制-XX:CompileThreshold10000#C2编译阈值-XX:TieredCompilation# 开启分层编译JDK8默认开启-XX:TieredStopAtLevel1# 停在C1阶段快速启动2.3 OSR栈上替换OSR 允许在方法还在执行时替换其代码OSR 场景 ┌──────────────────────────────────────────────────────────────────┐ │ │ │ 循环执行到第 5000 次 │ │ - JIT 触发编译 │ │ - 编译完成 │ │ - OSR解释器栈帧 → 编译后代码栈帧 │ │ │ │ 关键不需要等方法返回直接在循环中间切换 │ │ │ └──────────────────────────────────────────────────────────────────┘三、核心优化技术3.1 方法内联Method Inlining内联是 JIT 最重要的优化——用被调用方法的本体替换调用点// 内联前publicclassInlineDemo{publicintcalculate(){returnsum(1,2)sum(3,4);// 两次方法调用}privateintsum(inta,intb){returnab;}}// 内联后编译后的等效代码publicintcalculate(){return1234;// 无方法调用开销}内联的判断依据# JVM 内联策略# 热点方法 方法体不太大 内联# 虚方法调用多态 类型推断成功则内联否则保守处理# 参数控制-XX:MaxInlineLevel9# 最大内联层级-XX:FreqInlineSize325# 小方法优先内联-XX:Inline# 开启内联JDK 8 默认开启3.2 逃逸分析Escape Analysis逃逸分析判断对象的生命周期是否超出创建它的方法// 逃逸场景分析publicclassEscapeDemo{// 情况1不逃逸可优化publicvoidnoEscape(){ObjectobjnewObject();// 只在这个方法中使用obj.process();// JVM 可能栈上分配、标量替换、锁消除}// 情况2方法逃逸publicObjectmethodEscape(){ObjectobjnewObject();returnobj;// 返回给调用者 → 逃逸}// 情况3线程逃逸publicstaticObjectshared;// 静态变量publicvoidthreadEscape(){sharednewObject();// 线程间共享 → 逃逸}}逃逸分析的优化效果┌──────────────────────────────────────────────────────────────────┐ │ 逃逸分析后的优化 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 1. 栈上分配Stack Allocation │ │ 对象在栈上分配而非堆 → 减少 GC 压力 │ │ │ │ 2. 标量替换Scalar Replacement │ │ 对象拆解为多个标量基本类型 → 减少对象分配 │ │ class Point { int x, y; } │ │ → 编译后x, y 作为独立的寄存器变量 │ │ │ │ 3. 锁消除Lock Elision │ │ 如果对象不逃逸同步块被移除 │ │ synchronized(lockObj) { ... } → 去掉 synchronized │ │ │ └──────────────────────────────────────────────────────────────────┘3.3 其他优化技术常见 JIT 优化技术 ┌──────────────────────────────────────────────────────────────────┐ │ │ │ 循环展开Loop Unrolling │ │ for (int i0; i4; i) process(i); │ │ → process(0); process(1); process(2); process(3); │ │ → 减少循环判断开销增加并行优化机会 │ │ │ │ 常量折叠Constant Folding │ │ int x 2 3; → int x 5; │ │ │ │ 公共子表达式消除Common Subexpression Elimination │ │ if (a.b.c x a.b.c y) │ │ → tmp a.b.c; if (tmp x tmp y) │ │ │ │ 条件跳转优化Branch Prediction │ │ 热点分支优先提升 CPU 流水线效率 │ │ │ └──────────────────────────────────────────────────────────────────┘四、JIT 日志分析4.1 打印 JIT 编译日志# JDK 8-XX:PrintCompilation-XX:PrintInlining# 打印内联决策# JDK 9-Xlog:compilationinfo:filecompilation.log# 输出示例$ jstat-printcompilationpid10004.2 日志解读# JIT 编译日志示例 54 3 4 com.example.Calculator::calculate (42 bytes) 1 com.example.Calculator::add (10 bytes) inline (hot) 6 java.lang.Math::max (11 bytes) inline (hot) # 解读 # 54 相对时间秒 # 3 编译层级Tier 3 # 4 线程 ID # calculate 方法名 # 42 bytes 方法字节码大小 # inline 是否内联 # (hot) 内联原因热点方法4.3 内联日志分析内联决策速查 ┌──────────────────────────────────────────────────────────────────┐ │ inline (hot) │ 热点方法成功内联 │ │ inline (size) │ 方法太大未内联 │ │ inline (callee too large)│ 被调用方法太大 │ │ inline (call site not reached)│ 永不执行未内联 │ │ (discarded) │ 已内联但后来被去优化Deopt │ │ (estranged) │ 虚方法分派去优化 │ └──────────────────────────────────────────────────────────────────┘五、JIT 调优实践5.1 启动优化# 快速启动配置JDK 8JAVA_OPTS -XX:TieredCompilation -XX:TieredStopAtLevel1 # 停在 C1 阶段启动更快 -XX:ReservedCodeCacheSize240m # 预留足够 CodeCache # JDK 11 自动优化通常不需要手动配置5.2 峰值性能优化# 追求峰值性能JAVA_OPTS -XX:TieredCompilation -XX:TieredStopAtLevel4 # 完整编译到 C2 -XX:CompileThreshold10000 -XX:ReservedCodeCacheSize480m -XX:UseCodeCacheFlushing # 主动刷新 CodeCache 5.3 代码级优化建议// 1. 小方法优先内联建议 35 字节码// 好多个小方法privateintadd(inta,intb){returnab;}// 不好大方法难以内联privateBigResultcalculate(ListDatalist){// 1000 行代码...}// 2. 虚方法调用优化// 好final 类 / final 方法明确不需多态publicfinalclassCalculator{publicfinalintadd(...){...}}// 不好大量继承层次JIT 需要去虚化总结JIT 编译器是 JVM 性能的核心。通过分层编译C1 快速响应C2 深度优化两者配合实现启动速度与峰值性能的平衡。方法内联和逃逸分析是最重要的优化技术——前者减少调用开销后者开启栈上分配、标量替换、锁消除等激进优化。阅读 JIT 日志是调优的基础-XX:PrintCompilation和-XX:PrintInlining是两个核心参数。系列导航上一篇【JVM深度解析】第18篇案例五GC停顿优化与低延迟改造下一篇【JVM深度解析】第20篇字节码执行与ASM框架系列目录JVM深度解析系列全集参考资料Oracle: JIT CompilerOpenJDK JIT SourcesJVM Anatomy Quarks - JITUnderstanding JIT CompilationTiered Compilation in JVM