强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 1C1简单编译无性能分析,编译快
Level 2C1有限分析部分 Profile 收集
Level 3C1完整分析完整 Profile 收集
Level 4C2/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 的快速编译器,设计目标:

  1. 快速编译:编译时间短,减少预热延迟
  2. 适度优化:进行基本优化但不过度
  3. 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 的优化编译器,设计目标:

  1. 极致性能:生成尽可能高效的机器码
  2. 全局优化:方法级别的全局分析和优化
  3. 深度内联:激进的内联策略

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 优化能力

优化技术C2Graal
部分逃逸分析
更精确的内联决策基础改进
向量化支持良好更好
异常处理优化基础改进
动态语言支持有限优秀
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.arraycopySIMD 优化的内存拷贝
数学Math.sin/cos/exp/log使用 CPU 指令
字符串String.equals/compareToSIMD 比较
位操作Integer.bitCount/numberOfTrailingZerosPOPCNT/BSF 指令
原子操作Unsafe.compareAndSwap*CAS 指令
CRCCRC32.updateCRC32 指令
AESAESCrypt.encrypt/decryptAES-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 本章小结

关键要点

  1. 分层编译:解释执行 → C1 快速编译 → C2/Graal 深度优化
  2. 逃逸分析:标量替换、锁消除、栈上分配
  3. 方法内联:最重要的优化,消除调用开销
  4. 循环优化:展开、向量化、边界检查消除
  5. Intrinsics:手写优化代码,直接使用 CPU 指令

调优清单

  • 确认分层编译已启用
  • 检查内联阈值是否合理
  • 确认逃逸分析已启用
  • 使用 JMH 进行基准测试
  • 使用 JITWatch 分析编译结果
  • 检查是否有过多的去优化

7.11 扩展阅读


上一章: 第6章 - LLVM JIT 下一章: 第8章 - C# RyuJIT