JIT 编译与业务结合实战教程 / 第7章:Java HotSpot
第7章:Java HotSpot
“HotSpot 是工业界最成熟、部署最广泛的 JIT 编译器,理解它就是理解 Java 性能的核心。”
7.1 HotSpot 概述
Java HotSpot 虚拟机是 Oracle 官方的 Java 虚拟机实现,自 1999 年随 JDK 1.3 发布以来,一直是 Java 生态的核心运行时。HotSpot 名称来源于其"热点探测"技术——只对频繁执行的热点代码进行深度优化。
7.1.1 HotSpot 整体架构
┌─────────────────────────────────────────────────────────────────┐
│ HotSpot 虚拟机架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Java 字节码 (.class) │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 类加载子系统 │ ← 加载、验证、准备、解析 │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 解释器 │ ← 模板解释器(Template Interpreter) │
│ │ (Template) │ 直接执行字节码 │
│ └────────┬─────────┘ │
│ │ 收集 Profile │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 分层编译 (Tiered Compilation) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐│ │
│ │ │ C1 │ │ C2/Graal │ │ C2/Graal ││ │
│ │ │ 快速编译 │ → │ 优化编译 │ → │ 完全优化 ││ │
│ │ │ Level 1-3 │ │ Level 4 │ │ Level 4 ││ │
│ │ └────────────┘ └────────────┘ └────────────┘│ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ 垃圾收集器 │ ← G1, ZGC, Shenandoah │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.1.2 分层编译详解
HotSpot 的分层编译(Tiered Compilation)是其核心特性,将编译过程分为多个层次:
| 编译层级 | 编译器 | 说明 | 特点 |
|---|---|---|---|
| Level 0 | 解释器 | 初始执行 | 收集基本 Profile |
| Level 1 | C1 | 简单编译 | 无性能分析,编译快 |
| Level 2 | C1 | 有限分析 | 部分 Profile 收集 |
| Level 3 | C1 | 完整分析 | 完整 Profile 收集 |
| Level 4 | C2/Graal | 完全优化 | 峰值性能 |
# 查看分层编译日志
java -XX:+PrintCompilation \
-XX:+TieredCompilation \
-XX:CompileThreshold=1000 \
MyApp
# 输出格式:
# 时间戳 编译ID 层级 方法名 (字节码大小)
# 1234 1 3 java.lang.String::hashCode (55 bytes)
# 1235 2 4 java.lang.String::hashCode (55 bytes) # 重新编译到 Level 4
7.1.3 编译阈值配置
# 分层编译相关参数
-XX:+TieredCompilation # 启用分层编译(默认开启)
-XX:TieredStopAtLevel=4 # 停止在哪个层级
# C1 相关参数
-XX:Tier0ProfilingStartPercentage=0 # 开始收集 Profile 的百分比
-XX:MaxInlineSize=35 # C1 内联大小限制
# C2 相关参数
-XX:CompileThreshold=10000 # 编译阈值(方法调用次数)
-XX:OnStackReplacePercentage=140 # OSR 编译阈值百分比
-XX:MaxInlineSize=325 # C2 内联大小限制
# 实验:关闭分层编译只使用解释器
java -Xint MyApp
# 实验:只使用 C1
java -XX:TieredStopAtLevel=1 MyApp
# 实验:只使用 C2
java -XX:-TieredCompilation MyApp
7.2 C1 编译器
7.2.1 C1 的设计目标
C1(Client Compiler)是 HotSpot 的快速编译器,设计目标:
- 快速编译:编译时间短,减少预热延迟
- 适度优化:进行基本优化但不过度
- Profile 收集:为后续 C2 编译提供运行时信息
7.2.2 C1 编译管线
┌─────────────────────────────────────────────────────────────────┐
│ C1 编译管线 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 字节码 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 构建 HIR │ ← 高级中间表示 │
│ │ (GraphBuilder) │ │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 优化遍 │ │
│ │ ├─ 常量折叠 │ │
│ │ ├─ 死代码消除 │ │
│ │ ├─ 内联 │ │
│ │ └─ null 检查消除│ │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 构建 LIR │ ← 低级中间表示 │
│ │ (LIRGenerator) │ │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 寄存器分配 │ ← 线性扫描分配 │
│ │ (LinearScan) │ │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 代码生成 │ ← 生成机器码 │
│ │ (CodeEmit) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2.3 C1 IR 结构
// C1 HIR 节点示例(概念表示)
// 指令层次结构
// 指令基类
class Instruction {
enum InstructionType {
Constant, // 常量
LocalAccess, // 局部变量访问
FieldAccess, // 字段访问
Invoke, // 方法调用
Arithmetic, // 算术运算
Logic, // 逻辑运算
Compare, // 比较
Branch, // 分支
Return, // 返回
// ...
};
};
// 示例:一个简单的加法
// int result = a + b;
//
// HIR 表示:
// [LoadLocal a] ──┐
// ├── [Add] ── [StoreLocal result]
// [LoadLocal b] ──┘
7.3 C2 编译器
7.3.1 C2 的设计目标
C2(Server Compiler)是 HotSpot 的优化编译器,设计目标:
- 极致性能:生成尽可能高效的机器码
- 全局优化:方法级别的全局分析和优化
- 深度内联:激进的内联策略
7.3.2 C2 编译管线
┌─────────────────────────────────────────────────────────────────┐
│ C2 编译管线 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 字节码 + Profile 数据 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 构建 Sea of │ ← 基于图的 IR │
│ │ Nodes IR │ │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 理想图优化 │ ← 多趟优化 │
│ │ ├─ 全局值编号 │ │
│ │ ├─ 循环优化 │ │
│ │ ├─ 内联 │ │
│ │ ├─ 逃逸分析 │ │
│ │ ├─ 向量化 │ │
│ │ └─ 匹配 │ │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 寄存器分配 │ ← 图着色分配 │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 指令选择 │ ← 匹配目标机器指令 │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 代码生成 │ ← 生成机器码 │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.3.3 C2 的 Sea of Nodes IR
C2 使用 Sea of Nodes IR,这是一种基于图的表示,每个节点代表一个操作或值。
// Java 源码
public int compute(int x, int y) {
int a = x + y;
int b = x * 2;
return a - b;
}
// C2 Sea of Nodes IR (概念表示)
//
// [Parm x] ──┐ ┌── [Parm x]
// │ │
// ▼ ▼
// [Parm y] → [Add] [Con 2] → [Mul]
// │ │
// ▼ ▼
// └──────→ [Sub] ←────┘
// │
// ▼
// [Return]
# 查看 C2 编译的 IR (需要 debug build)
java -XX:+PrintOptoAssembly MyApp
# 使用 Ideal Graph Visualizer (IGV)
java -XX:+UnlockDiagnosticVMOptions \
-XX:+PrintIdeal \
-XX:PrintIdealGraphLevel=1 \
MyApp
7.4 Graal 编译器
7.4.1 作为 C2 的替代
Graal 编译器可以作为 C2 的替代品,通过 JVMCI 接口集成到 HotSpot。
# 使用 Graal 替代 C2
java -XX:+UnlockExperimentalVMOptions \
-XX:+EnableJVMCI \
-XX:+UseJVMCICompiler \
-XX:+UseJVMCICompiler \
MyApp
# 仅在最后一个编译层使用 Graal
java -XX:+UnlockExperimentalVMOptions \
-XX:+EnableJVMCI \
-XX:+UseJVMCICompiler \
-XX:TieredStopAtLevel=4 \
MyApp
7.4.2 Graal vs C2 优化能力
| 优化技术 | C2 | Graal |
|---|---|---|
| 部分逃逸分析 | ✗ | ✓ |
| 更精确的内联决策 | 基础 | 改进 |
| 向量化支持 | 良好 | 更好 |
| 异常处理优化 | 基础 | 改进 |
| 动态语言支持 | 有限 | 优秀 |
| Profile-guided | 基础 | 更精确 |
7.5 逃逸分析
7.5.1 逃逸分析概述
逃逸分析(Escape Analysis)是 C2 编译器的核心优化之一,分析对象是否"逃逸"出当前方法或线程。
┌─────────────────────────────────────────────────────────────────┐
│ 逃逸分析结果类型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. NoEscape (不逃逸) │
│ └─ 对象只在当前方法内使用 │
│ └─ 可以进行标量替换、栈上分配 │
│ │
│ 2. ArgEscape (参数逃逸) │
│ └─ 对象传递给其他方法但不逃逸出线程 │
│ └─ 可以进行锁消除 │
│ │
│ 3. GlobalEscape (全局逃逸) │
│ └─ 对象逃逸出当前线程 │
│ └─ 无法优化,必须堆上分配 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.5.2 标量替换
// 标量替换示例
public class EscapeAnalysisDemo {
// 对象不逃逸 - 可以标量替换
public int scalarReplacement() {
Point p = new Point(3, 4); // 对象不逃逸
return p.x + p.y; // 替换为: return 3 + 4;
}
// C2 优化后等价于:
// public int scalarReplacement() {
// return 7; // 常量折叠
// }
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
// 对象逃逸 - 无法优化
private Point savedPoint;
public void globalEscape() {
Point p = new Point(1, 2);
savedPoint = p; // 逃逸到实例字段
}
// 部分逃逸
public int partialEscape(boolean condition) {
Point p = new Point(1, 2);
if (condition) {
return p.x + p.y; // 对象可以标量替换
}
return 0; // 另一条路径不使用 p
}
}
# 查看逃逸分析结果
java -XX:+DoEscapeAnalysis \
-XX:+PrintEscapeAnalysis \
-XX:+PrintEliminateAllocations \
MyApp
7.5.3 锁消除
// 锁消除示例
public class LockElimination {
// StringBuffer 的锁可以被消除
public String buildString(String[] items) {
StringBuffer sb = new StringBuffer(); // sb 不逃逸
for (String item : items) {
sb.append(item); // 锁操作可以消除
}
return sb.toString();
}
// C2 优化后等价于:
// public String buildString(String[] items) {
// StringBuilder sb = new StringBuilder(); // 无锁版本
// for (String item : items) {
// sb.append(item);
// }
// return sb.toString();
// }
// 显式同步块也可以消除
public int compute(int x) {
Object lock = new Object(); // lock 不逃逸
synchronized (lock) { // 锁可以消除
return x * 2;
}
}
}
7.5.4 栈上分配
// 栈上分配示例
public class StackAllocation {
// 在某些情况下,对象可以在栈上分配
// 栈上分配的对象无需 GC
public long stackAllocExample() {
long sum = 0;
for (int i = 0; i < 100000; i++) {
// 这个对象可以栈上分配(理想情况)
// 实际上 HotSpot 主要使用标量替换而非栈上分配
Value v = new Value(i);
sum += v.x;
}
return sum;
}
static class Value {
final int x;
Value(int x) { this.x = x; }
}
}
7.6 方法内联
7.6.1 内联的重要性
方法内联(Method Inlining)是最重要的 JIT 优化之一。它将被调用方法的代码直接插入调用点,消除调用开销并为后续优化创造机会。
// 内联示例
public class InliningDemo {
// 小方法 - 容易被内联
public int square(int x) {
return x * x;
}
// 调用方
public int compute(int a, int b) {
return square(a) + square(b);
}
// 内联后等价于:
// public int compute(int a, int b) {
// return (a * a) + (b * b);
// }
}
7.6.2 内联决策因素
| 因素 | 说明 |
|---|---|
| 方法大小 | 字节码大小不超过阈值 |
| 调用频率 | 调用次数越多越可能内联 |
| 调用深度 | 内联深度有限制 |
| Profile 数据 | 类型信息是否确定 |
| 是否虚方法 | 虚方法需要类型推测 |
# 内联相关参数
-XX:MaxInlineSize=35 # 小方法内联阈值(字节码大小)
-XX:FreqInlineSize=325 # 频繁方法内联阈值
-XX:MaxInlineLevel=9 # 最大内联深度
-XX:InlineSmallCode=2000 # 已编译代码大小阈值
-XX:MaxInlineSize=35 # C1 内联阈值
-XX:MaxInlineSize=325 # C2 内联阈值
# 查看内联决策
java -XX:+UnlockDiagnosticVMOptions \
-XX:+PrintInlining \
MyApp
7.6.3 虚方法内联
// 虚方法内联
public class VirtualInlining {
// 接口/虚方法
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
public double area() { return Math.PI * radius * radius; }
}
class Rectangle implements Shape {
double width, height;
public double area() { return width * height; }
}
// 如果 Profile 显示主要调用 Circle.area()
// C2 会推测优化为 Circle.area() 的内联版本
public double totalArea(Shape[] shapes) {
double total = 0;
for (Shape s : shapes) {
total += s.area(); // 可能被内联
}
return total;
}
// 使用 final 方法可以确保内联
class OptimizedCircle implements Shape {
double radius;
public final double area() { // final 确保不会被覆盖
return Math.PI * radius * radius;
}
}
}
7.7 循环优化
7.7.1 循环优化技术
public class LoopOptimizations {
// 1. 循环展开
// 原始
public void loopUnroll(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2;
}
}
// 展开后 (概念)
// for (int i = 0; i + 3 < arr.length; i += 4) {
// arr[i] = arr[i] * 2;
// arr[i+1] = arr[i+1] * 2;
// arr[i+2] = arr[i+2] * 2;
// arr[i+3] = arr[i+3] * 2;
// }
// 2. 循环不变量外提
public void loopInvariant(int[] arr, int scale) {
for (int i = 0; i < arr.length; i++) {
// scale * 100 是循环不变量,可以外提
arr[i] = scale * 100;
}
}
// 优化后:
// int temp = scale * 100;
// for (int i = 0; i < arr.length; i++) {
// arr[i] = temp;
// }
// 3. 范围检查消除
public int rangeCheck(int[] arr, int index) {
if (index >= 0 && index < arr.length) {
return arr[index]; // 边界检查可以消除
}
throw new IndexOutOfBoundsException();
}
// 4. 向量化 (SIMD)
public void vectorize(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = a[i] + b[i]; // 可以使用 SIMD 指令
}
}
}
# 查看循环优化
java -XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintInlining \
-XX:+PrintOptoAssembly \
MyApp
# 检查向量化
java -XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintIntrinsics \
MyApp
7.7.2 循环优化参数
# 循环展开参数
-XX:+UseLoopPredicate # 循环谓词(默认开启)
-XX:+UnrollLimitLoop=16 # 循环展开限制
-XX:LoopUnrollLimit=60 # 循环展开复杂度限制
# 向量化参数
-XX:+UseVectorApiIntrinsics # 向量 API 内置
-XX:+UseVectorCmov # 向量条件移动
7.8 内置方法(Intrinsics)
7.8.1 什么是 Intrinsics
HotSpot 为特定的 Java 方法提供了手写的机器码实现(Intrinsics),绕过常规编译直接使用优化的机器码。
// Intrinsics 示例
public class IntrinsicsDemo {
// System.arraycopy - 手写的优化内存拷贝
public void arrayCopy(int[] src, int[] dst) {
System.arraycopy(src, 0, dst, 0, src.length);
}
// String.equals - 针对字符串优化的比较
public boolean stringEquals(String a, String b) {
return a.equals(b);
}
// Math.max/min - 直接使用 CPU 指令
public int max(int a, int b) {
return Math.max(a, b);
}
// Integer.bitCount - 使用 POPCNT 指令
public int bitCount(int x) {
return Integer.bitCount(x);
}
// Unsafe 操作 - 直接内存访问
// VarHandle (Java 9+) - 原子操作
}
7.8.2 常见 Intrinsics
| 类别 | 方法 | 优化说明 |
|---|---|---|
| 数组 | System.arraycopy | SIMD 优化的内存拷贝 |
| 数学 | Math.sin/cos/exp/log | 使用 CPU 指令 |
| 字符串 | String.equals/compareTo | SIMD 比较 |
| 位操作 | Integer.bitCount/numberOfTrailingZeros | POPCNT/BSF 指令 |
| 原子操作 | Unsafe.compareAndSwap* | CAS 指令 |
| CRC | CRC32.update | CRC32 指令 |
| AES | AESCrypt.encrypt/decrypt | AES-NI 指令 |
# 查看 Intrinsics 使用
java -XX:+UnlockDiagnosticVMOptions \
-XX:+PrintIntrinsics \
MyApp
7.9 性能分析和调优
7.9.1 JIT 编译日志
# 基本编译日志
java -XX:+PrintCompilation MyApp
# 详细编译日志(需要 debug build)
java -XX:+UnlockDiagnosticVMOptions \
-XX:+LogCompilation \
-XX:LogFile=hotspot.log \
MyApp
# 使用 JITWatch 工具分析
# https://github.com/AdoptOpenJDK/jitwatch
7.9.2 常用调优参数
# 编译阈值
-XX:CompileThreshold=10000 # 方法调用次数阈值
-XX:OnStackReplacePercentage=140 # OSR 编译阈值
# 内联
-XX:MaxInlineSize=35 # 小方法内联阈值
-XX:FreqInlineSize=325 # 频繁方法内联阈值
-XX:MaxInlineLevel=9 # 最大内联深度
# 逃逸分析
-XX:+DoEscapeAnalysis # 启用逃逸分析(默认开启)
-XX:+EliminateAllocations # 启用标量替换(默认开启)
-XX:+EliminateLocks # 启用锁消除(默认开启)
# 分层编译
-XX:+TieredCompilation # 启用分层编译
-XX:TieredStopAtLevel=4 # 停止在哪个层级
7.9.3 JMH 基准测试
// 使用 JMH 进行基准测试
import org.openjdk.jmh.annotations.*;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@Fork(2)
public class MyBenchmark {
private int[] data;
@Setup
public void setup() {
data = new int[10000];
for (int i = 0; i < data.length; i++) {
data[i] = i;
}
}
@Benchmark
public long sumArray() {
long sum = 0;
for (int v : data) {
sum += v;
}
return sum;
}
@Benchmark
public long sumStream() {
return Arrays.stream(data).asLongStream().sum();
}
}
# 运行 JMH 基准测试
mvn clean install
java -jar target/benchmarks.jar
# 使用 JMH 运行并收集 JIT 日志
java -XX:+PrintCompilation -jar target/benchmarks.jar
7.10 本章小结
关键要点
- 分层编译:解释执行 → C1 快速编译 → C2/Graal 深度优化
- 逃逸分析:标量替换、锁消除、栈上分配
- 方法内联:最重要的优化,消除调用开销
- 循环优化:展开、向量化、边界检查消除
- Intrinsics:手写优化代码,直接使用 CPU 指令
调优清单
- 确认分层编译已启用
- 检查内联阈值是否合理
- 确认逃逸分析已启用
- 使用 JMH 进行基准测试
- 使用 JITWatch 分析编译结果
- 检查是否有过多的去优化
7.11 扩展阅读
上一章: 第6章 - LLVM JIT 下一章: 第8章 - C# RyuJIT