知识模块
☕ Java 知识模块
四、JVM 深入理解
JIT 即时编译

JIT 即时编译

面试提问

"JVM 的 JIT 编译器是什么?它如何提升 Java 程序性能?"


核心概念

为什么需要 JIT?

Java 程序的执行过程:

源代码(.java) → 字节码(.class) → 解释执行 / JIT 编译 → 机器码执行

问题:解释执行效率低,每次都要翻译字节码

解决:JIT(Just-In-Time)即时编译器,将热点代码编译成机器码缓存,后续直接执行

JIT 的工作原理

┌─────────────────────────────────────────────────┐
│                    JVM 执行                      │
├─────────────────────────────────────────────────┤
│  方法调用                                        │
│     ↓                                           │
│  解释器执行 + 方法调用计数器                       │
│     ↓                                           │
│  热点代码?(计数器超过阈值)                       │
│     ↓ 是                                        │
│  JIT 编译 → 生成机器码 → 缓存到 CodeCache         │
│     ↓                                           │
│  后续调用直接执行机器码                            │
└─────────────────────────────────────────────────┘

HotSpot 的两种 JIT 编译器

1. C1 编译器(Client 编译器)

特点

  • 编译速度快
  • 优化程度低
  • 适合启动时间敏感的应用

优化内容

  • 局部优化(死代码消除、常量折叠)
  • 简单内联

2. C2 编译器(Server 编译器)

特点

  • 编译速度慢
  • 优化程度高
  • 适合长时间运行的服务端应用

优化内容

  • 全局优化
  • 逃逸分析
  • 循环优化
  • 积极内联

分层编译(Tiered Compilation)

JDK8 默认开启,结合 C1 和 C2 的优势:

第 0 层:解释执行
第 1 层:C1 编译(简单优化)
第 2 层:C1 编译(更多优化)
第 3 层:C1 编译(完整 profiling)
第 4 层:C2 编译(最高优化)
# 开启分层编译(JDK8 默认开启)
-XX:+TieredCompilation
 
# 指定编译器
-client    # 使用 C1
-server    # 使用 C2

热点探测

方法调用计数器

统计方法被调用的次数,超过阈值触发 JIT 编译。

# 方法调用阈值(Client 模式默认 1500,Server 模式默认 10000)
-XX:CompileThreshold=10000
 
# 分层编译时的阈值调整因子
-XX:Tier0CompileThreshold=1000
-XX:Tier1CompileThreshold=2000
-XX:Tier2CompileThreshold=3000
-XX:Tier3CompileThreshold=5000
-XX:Tier4CompileThreshold=10000

回边计数器

统计循环回边的次数,用于识别循环热点。

当循环执行次数超过阈值,触发栈上替换(OSR),在循环过程中替换为编译后的机器码。


JIT 核心优化技术

1. 方法内联(Inlining)

将被调用方法的代码直接嵌入调用处,消除方法调用开销。

优化前

public int add(int a, int b) {
    return a + b;
}
public int calculate(int x, int y) {
    return add(x, y) * 2;
}

优化后(内联)

public int calculate(int x, int y) {
    return (x + y) * 2;  // add 方法被内联
}

参数控制

-XX:MaxInlineSize=35      # 可内联方法的最大字节码大小
-XX:FreqInlineSize=325    # 频繁调用方法的最大字节码大小

2. 逃逸分析(Escape Analysis)

分析对象是否"逃逸"出方法或线程,进行针对性优化。

三种优化

优化条件效果
栈上分配对象不逃逸出方法对象在栈上分配,无需 GC
标量替换对象可分解用局部变量替代对象字段
锁消除对象不逃逸出线程消除同步锁

示例 - 标量替换

// 原代码
public void process() {
    Point p = new Point(1, 2);  // 对象不逃逸
    System.out.println(p.x + p.y);
}
 
// 优化后(标量替换)
public void process() {
    int x = 1;  // 直接用局部变量
    int y = 2;
    System.out.println(x + y);
}
# 开启逃逸分析(默认开启)
-XX:+DoEscapeAnalysis
 
# 开启标量替换(默认开启)
-XX:+EliminateAllocations

3. 锁优化

锁消除:逃逸分析发现对象不逃逸出线程,消除同步锁

public String concat(String s1, String s2) {
    StringBuffer sb = new StringBuffer();  // 局部对象,不逃逸
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}
// StringBuffer 的同步锁会被消除

锁膨胀:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

4. 循环优化

优化类型说明
循环展开减少循环次数,增加每次迭代的工作量
循环不变量外提将循环内不变的代码移到循环外
循环融合合并多个相邻的循环
// 优化前
for (int i = 0; i < n; i++) {
    a[i] = i * 2;
}
 
// 循环展开(展开因子为 2)
for (int i = 0; i < n; i += 2) {
    a[i] = i * 2;
    a[i + 1] = (i + 1) * 2;
}

5. 死代码消除

删除永远不会执行的代码或计算结果未使用的代码。

// 优化前
public int calculate(int x) {
    int a = 10;        // 未使用
    int b = x * 2;     // 未使用
    int c = x + 1;     // 实际返回值
    return c;
}
 
// 优化后
public int calculate(int x) {
    return x + 1;
}

CodeCache

JIT 编译后的机器码存储在 CodeCache 中。

# CodeCache 大小设置
-XX:InitialCodeCacheSize=160K      # 初始大小
-XX:ReservedCodeCacheSize=240M     # 最大大小(JDK8 默认 240M)
 
# CodeCache 满了会怎样?
# - JIT 停止编译新代码
# - 程序性能下降
# - 可能出现警告:CodeCache is full. Compiler has been disabled.

CodeCache 监控

# 打印 CodeCache 使用情况
-XX:+PrintCodeCache
 
# JDK9+ 使用分段 CodeCache
-XX:+SegmentedCodeCache

JIT 相关参数速查

编译控制

参数默认值说明
-XX:+TieredCompilation开启分层编译
-XX:CompileThreshold10000触发编译的调用次数
-XX:+PrintCompilation关闭打印编译信息

内联控制

参数默认值说明
-XX:MaxInlineSize35内联方法最大字节码大小
-XX:FreqInlineSize325频繁调用方法的内联限制

逃逸分析

参数默认值说明
-XX:+DoEscapeAnalysis开启逃逸分析
-XX:+EliminateAllocations开启标量替换
-XX:+EliminateLocks开启锁消除

JIT vs AOT

对比项JITAOT
编译时机运行时编译时
启动速度慢(需要预热)
峰值性能高(运行时优化)中等
动态特性支持有限支持
典型工具HotSpot JITGraalVM Native Image

GraalVM Native Image:将 Java 应用编译成原生可执行文件,启动时间毫秒级。


面试要点总结

问题答案要点
JIT 是什么?即时编译器,将热点代码编译成机器码缓存,提升执行效率
C1 和 C2 的区别?C1 编译快优化少,C2 编译慢优化多;分层编译结合两者
热点探测方式?方法调用计数器、回边计数器
JIT 主要优化?方法内联、逃逸分析(栈上分配/标量替换/锁消除)、循环优化
逃逸分析的作用?判断对象是否逃逸,进行栈上分配、标量替换、锁消除优化

相关题目

  1. 为什么 Java 说"解释执行",但性能可以接近 C++?
  2. 什么是方法内联?有什么好处?
  3. 逃逸分析能做哪些优化?
  4. 分层编译的层次划分是什么?

参考资料