Skip to content

热点代码探测(C1/C2/Graal/AOT)

什么是热点代码

热点代码(Hot Spot)是 JVM 对「被频繁执行的代码」的称呼。JIT 编译器只编译热点代码,而不是全部代码——这是 JIT 的核心策略。

为什么要区分?因为编译有开销,如果对所有代码都进行 JIT 编译:

  1. 编译本身耗时,程序启动变慢
  2. Code Cache 空间有限,全部编译装不下
  3. 大多数代码只执行一次或几次,编译投入不划算

热点探测的方法

1. 方法调用计数器(Method Invocation Counter)

每个方法有一个计数器,记录被调用的次数。超过阈值就触发 JIT 编译。

java
public class MethodCounter {
    public static void main(String[] args) {
        // 这个方法被调用一次,计数器 +1
        hotMethod();
    }

    static void hotMethod() {
        // 如果被调用超过阈值(如 10000 次)
        // → 触发 JIT 编译
    }
}

HotSpot 的方法调用计数器阈值:

编译器默认阈值说明
C1(Client)1,500快速编译,允许较高的阈值容忍
C2(Server)10,000激进优化,需要更多调用数据

2. 回边计数器(Backedge Counter)

回边计数器记录循环体的执行次数。循环的「回边」是从循环体末尾回到开头的指令。

java
public class BackedgeCounter {
    public static void main(String[] args) {
        int sum = 0;
        // 这个循环体被执行 1000000 次
        // 回边计数器不断累加
        // 超过阈值时 → 触发 JIT 编译
        for (int i = 0; i < 1_000_000; i++) {
            sum += i;
        }
    }
}

3. 方法调用计数器 + 回边计数器

HotSpot 同时使用两种计数器,更全面地探测热点:

热点探测机制:

  ├── 方法调用计数器(Method Counter)
  │     记录:invokevirtual / invokestatic / invokespecial
  │     阈值:C1 = 1500, C2 = 10000

  └── 回边计数器(Backedge Counter)
        记录:for/while/do 循环的回边
        阈值:同样基于方法调用阈值计算

分层编译与热点层级

分层编译把 JIT 编译分为多个层级,每个层级有不同的热点探测策略:

Tier 0:纯解释执行

  • 不做任何编译
  • 收集方法调用计数器和回边计数器
  • 触发条件:方法开始执行时立即进入

Tier 1:C1 快速编译

  • 快速编译,不做激进优化
  • 启用基础的 profiling(调用计数、分支信息)
  • 触发条件:方法调用计数 > 1500(C1 阈值)

Tier 2/3:C1 完整编译

  • 更详细的 profiling(类型信息、分支预测)
  • 编译速度稍慢,但收集更多优化信息
  • 触发条件:Tier 1 持续热点

Tier 4:C2 完全编译

  • 最激进的优化
  • 基于 Tier 2/3 收集的 profile 信息
  • 触发条件:调用计数 > 10000(C2 阈值)
调用次数

10k ─┼──────────────────────────────────── C2 编译(峰值性能)
    │  ╭──────────────────────────────────╮
1.5k ─┼──── C1 编译 ─→ C1 完整编译 ─→ C2 编译 ─╯
    │  ↑
    │ 解释执行

    └──+──────→ 时间/执行次数

C1 vs C2:两种编译器的人格

HotSpot 有两个 JIT 编译器,它们有完全不同的设计目标:

C1(Client Compiler)

C1 是客户端编译器,目标是快速编译

bash
# 启用 C1
java -client MyApp

# 只用 C1(关闭 C2)
java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 MyApp

特点:

  • 编译速度快,但优化程度低
  • 适合启动阶段或短期程序
  • 在分层编译模式下作为第一层编译

C2(Server Compiler)

C2 是服务端编译器,目标是峰值性能

bash
# 启用 C2
java -server MyApp

# 只用 C2(跳过 C1)
java -XX:+TieredCompilation -XX:TieredStopAtLevel=4 MyApp

特点:

  • 编译速度慢,但优化程度高
  • 内联、去虚化、逃逸分析等激进优化
  • 适合长期运行的服务端应用

C1 和 C2 的核心区别

维度C1(Client)C2(Server)
设计目标快速编译峰值性能
编译时间
优化程度轻量级激进
适用场景短期程序、客户端长期服务
内联策略保守激进(更大的内联阈值)
逃逸分析有限完整

Graal:新一代编译器

从 JDK 9 开始,HotSpot 可以使用 Graal 编译器替代 C2(通过 -XX:+UseGraalCompiler,JDK 10+)。

Graal 的特点

维度C2Graal
语言C++Java(AOT 编译)
优化能力强(某些场景更强)
编译速度更快(某些场景)
JVM 集成内嵌 HotSpot可插拔
Truffle 框架不支持支持(语言实现框架)

GraalVM

GraalVM 是基于 Graal 编译器构建的通用运行时:

GraalVM 架构:
  ┌──────────────────────────────────────┐
  │            上层应用                    │
  │  Java / JavaScript / Python / Ruby  │
  └──────────────┬───────────────────────┘

  ┌──────────────▼───────────────────────┐
  │         GraalVM Compiler               │
  │  (用 Java 实现的 JIT/AOT 编译器)       │
  └──────────────┬───────────────────────┘

  ┌──────────────▼───────────────────────┐
  │     LLVM / JVMCI / Substrate VM      │
  └──────────────────────────────────────┘

GraalVM 可以:

  • 高性能执行:用 Graal JIT 替换 C2
  • 多语言支持:在同一个 JVM 上运行 Python、JS、R 等
  • Native Image:AOT 编译成独立可执行文件

AOT:编译的极致提前

AOT(Ahead-Of-Time)编译在程序运行之前完成编译,彻底消灭 JIT 阶段。

bash
# JDK 9+ 使用 jaotc 预编译
jaotc --output libHelloWorld.so HelloWorld.class

# 运行使用 AOT 编译的代码
java -XX:AOTLibrary=./libHelloWorld.so HelloWorld

AOT vs JIT

维度JIT 编译AOT 编译
编译时机运行时运行前
运行时信息✅ 可用(profile-guided)❌ 不可用
启动速度慢(需要预热)快(直接运行机器码)
峰值性能高(JIT 优化)较低
适用场景服务器长期运行短期程序、容器

热点探测参数调优

bash
# 查看热点探测阈值
java -XX:+PrintFlagsFinal -version | grep -i threshold

# 调整方法调用计数阈值
java -XX:CompileThreshold=10000 MyApp

# 调整分层编译停层级
java -XX:TieredStopAtLevel=1 MyApp   # 只用 C1
java -XX:TieredStopAtLevel=4 MyApp   # 用 C1 + C2

# 调整 C1/C2 的切换阈值
java -XX:TieredCompilation \
     -XX:Tier3LoadFeedback=2000 \
     -XX:Tier2CompileThreshold=2000 \
     MyApp

# 关闭分层编译(使用纯 C2)
java -Xcomp MyApp

本节小结

热点代码探测的核心要点:

探测方式说明
方法调用计数器记录方法被调用次数,超过阈值触发编译
回边计数器记录循环体执行次数
分层编译Tier 0~4,从解释到 C1 到 C2
C1 vs C2C1 快速编译,C2 峰值优化
GraalJava 实现的 JIT/AOT 编译器
AOT程序运行前完成编译,无 JIT 阶段

理解热点探测和分层编译,是理解 JVM 性能调优的关键。

到这里,「执行引擎」专题全部完成。接下来进入 String 专题(JVM 视角),首先看 String 不可变性/底层结构/内存分配

基于 VitePress 构建