Skip to content

07.2 - C1 与 C2 编译器 & 分层编译

定位: HotSpot 的两大 JIT 编译器——C1 求快(快速编译),C2 求好(深度优化),分层编译在二者之间架设"进度阶梯" 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: C1 vs C2 对比、分层编译各层级作用、去优化原因

一、这是什么?为什么需要它们?

JIT 编译器将热点字节码编译为机器码。但不同的代码对编译速度和质量有不同要求

方法启动后不久就变成热点 (如 Servlet 的 doGet):
  我需要快速出优化代码 → C1 编译即可, 够快了

方法启动很久后才变成热点 (如复杂的排序算法):
  我值得花时间深度优化 → 等 C2 编译, 追求极致性能

方法一直在解释执行 (如不常用的错误处理):
  我根本不需要编译 → 继续在解释器跑

C1 (Client Compiler):以编译速度优先,适用于客户端应用或对启动时间敏感的场景。

  • 编译快(亚毫秒到几十毫秒/方法)
  • 优化适度(~100 种优化)
  • 代码质量"够好"

C2 (Server Compiler):以代码质量优先,适用于长时间运行的服务器。

  • 编译慢(秒级/方法)
  • 优化激进(~1000 种优化)
  • 代码质量"接近 hand-written assembly"

两者都不完美:C1 编译快但代码不够快,C2 代码快但编译等待太久。于是有了分层编译——先 C1 编译让你跑起来,再 C2 深度优化到极致。

二、原理拆解

2.1 C1 编译器——快速出活

C1 的编译管线设计围绕"快"展开:

C1 编译管线:
┌────────────┐   ┌────────────┐   ┌────────────┐   ┌────────────┐   ┌────────────┐
│ Bytecode   │ → │ HIR        │ → │ LIR        │ → │ 寄存器分配   │ → │ 机器码生成   │
│ Input      │   │ (High IR)  │   │ (Low IR)   │   │ (Register   │   │ (CodeGen)  │
│            │   │            │   │            │   │ Allocation) │   │            │
└────────────┘   └────────────┘   └────────────┘   └────────────┘   └────────────┘
                        │                  │               │
                        ▼                  ▼               ▼
                    字节码→SSA图         HIR→线性IR       线性扫描分配寄存器
                    基本优化:           消除虚指令      (快于图着色算法)
                    - 常量折叠           - 内存操作优化
                    - 死代码消除         - 窥孔优化
                    - 通用子表达式消除

C1 的优化清单(精简版~100 种中的代表性):

  • 常量折叠:int x = 2 * 3int x = 6
  • 死代码消除:未被使用的变量赋值 → 移除
  • 无用代码消除:不可达分支 → 移除
  • 方法内联:小方法(≤35 字节)直接内联
  • 简单循环展开:小循环适度展开
  • 窥孔优化:相邻的指令合并或替换为更高效的序列

2.2 C2 编译器——慢工出细活

C2 的编译管线建立在 Ideal Graph(Sea-of-Nodes IR)之上——这是 C2 区别于 C1 的核心:

C2 编译管线:
┌────────────┐   ┌──────────────────────────┐   ┌────────────┐   ┌────────────┐
│ Bytecode   │ → │ 构建 Ideal Graph          │ → │ 匹配/指令    │ → │ 寄存器分配   │
│ Input      │   │ (Sea-of-Nodes)           │   │ 选择        │   │ (Global    │
│            │   │                          │   │ (Matching)  │   │  RA)       │
└────────────┘   └──────────────────────────┘   └────────────┘   └────────────┘
                        │            ▲                  │               │
                        ▼            └─── 多轮迭代 ──────┘               ▼
                ┌──────────────────────────────────────┐          ┌────────────┐
                │  优化主循环 (反复迭代至收敛)           │          │ 机器码生成   │
                │  ┌────────────────────────────────┐  │          │ (CodeGen)  │
                │  │ 内联 → 逃逸分析 → 循环优化 →    │  │          │            │
                │  │ 锁消除 → 标量替换 → GVN →       │  │          │            │
                │  │ 类型分析 → 条件消除 → 空值检查消除│  │          │            │
                │  └────────────────────────────────┘  │          │            │
                └──────────────────────────────────────┘          └────────────┘

Ideal Graph(Sea-of-Nodes) 的精髓:数据流和控制流融合在一个图中,每个 node 代表一个操作,边代表数据或控制依赖。设计目标是让优化 pass 能够全局重排操作,不受原始字节码顺序的限制。

C2 的关键优化(~1000 种中的重量级):

C2 优化层次 (从最重要到次重要):

┌───────────────────────────────────────────────────────────┐
│ ████████████████████████████████ 方法内联                    │
│    一切优化的基础——打破方法边界才能看到全局                      │
├───────────────────────────────────────────────────────────┤
│ ████████████████████████ 逃逸分析 + 标量替换                │
│    消除不必要的对象分配 —— 最有效的"零GC"方案                  │
├───────────────────────────────────────────────────────────┤
│ ████████████████████ 循环优化                               │
│    循环展开、循环分离、循环翻转、循环逆转                       │
├───────────────────────────────────────────────────────────┤
│ ████████████████ 全局值编号 (GVN)                           │
│    发现并消除重复计算                                        │
├───────────────────────────────────────────────────────────┤
│ ████████████████ 锁消除 + 锁粗化                            │
│    消除不必要的同步开销                                      │
├───────────────────────────────────────────────────────────┤
│ ████████████ 边界检查消除                                    │
│    已知范围内的数组访问跳过越界检查                            │
├───────────────────────────────────────────────────────────┤
│ ████████████ 虚调用优化 (Devirtualization)                  │
│    将多态调用转为直接调用 (基于类型 profiling)                 │
├───────────────────────────────────────────────────────────┤
│ ███████████ 自动装箱消除                                    │
│    消除 int ↔ Integer 的隐式转换开销                        │
├───────────────────────────────────────────────────────────┤
│ ████ 条件传播与常量折叠                                     │
│    静态确定的分支直接消除                                    │
└───────────────────────────────────────────────────────────┘

2.3 分层编译——5 级的精妙设计

为什么需要 5 级,而不是直接 C1 → C2 两级? 因为 profiling 的收集是逐步展开的——太早收集 low-quality profile,太晚收集浪费编译机会。

分层编译的 5 级在时间线上的演进:

方法首次调用


Level 0 ─────────────────── 纯解释执行
│  - 无编译, 无 profiling              代码从冷到热的"检测阶段"
│  - 仅统计调用次数和回边次数             ──────────────
│                                     - 大多数方法停留在 L0
    │                                     (冷代码, 永远不触发编译)
    │ 调用次数达阈值 (或回边达 OSR 阈值)

Level 1 ──┐  C1 编译 ──┐  无 profiling
          │             │            (只有少数"伪热点"会到 L1)
          │             │
Level 2 ──┤  C1 编译 ──┤  基础 profiling (调用/回边计数)
          │             │            (达到基本热度)
          │             │
Level 3 ──┤  C1 编译 ──┤  完整 profiling
          │             │  - 类型 profile (receiver type 分布)  关键层级!
          │             │  - 分支 profile (branch taken/not)   为 C2 收集数据
          │             │  - 方法调用计数
          │             │
          ▼             ▼
Level 4 ──┘  C2 编译 ──┘  使用 L3 收集的 profiling 进行极限优化

关键规则

转换条件说明
L0 → L3方法调用计数达阈值大多数热点走 L0 → L3 → L4 路径
L3 → L4C2 编译队列空闲(或 C2 抢占)C2 用 L3 的 profile 做深度优化
L0 → L2C1 队列忙且 C2 队列空闲降级到 L2(少收集 profile)
L0 → L1方法极小(可无 profile 优化)如 getter/setter、Object::<init>
L4 → L0去优化触发投机假设失效 → 回退解释执行

L3 为什么是关键层级:C2 无法凭空优化——它需要知道"这个接口调用 99% 都是 ArrayList"、"这个 if 分支 95% 走 true"。L3 的完整 profiling 就是 C2 的"眼睛",没有它 C2 只能做保守优化。

为何存在 L1 和 L2?

  • L1:极小方法(如 getter/setter)无需 profile 就能优化——直接 C1 无 profiling 编译,甚至可能被直接内联到 L4 编译的方法中而不需要单独 L4
  • L2:C1 队列拥塞时的降级方案——收基础 profile,后续可升到 L3 或直接到 L4

2.4 去优化 (Deoptimization)

去优化是 JIT 安全的保障——编译器基于 profiling 做"投机优化",但不保证假设永远成立

去优化场景示例:

➊ 类型假设失败
C2 编译时基于 profiling: "mystery.toString() 99% 是 String.toString()"
→ 内联 String.toString() 并插入类型检查 guard
→ 运行时: obj 突然变成 StringBuilder! guard 失败!
→ 去优化: 回退解释执行, 重新收集 profile

➋ 类加载导致去优化
C2 编译时认为: "只有子类 A 和 B"
→ 运行时: 加载了子类 C, 之前的编译假设不再安全
→ 去优化: 回退, 重新收集包含 C 的 profile 后重新编译

去优化处理流程:
┌────────┐   ┌────────────┐   ┌──────────┐   ┌──────────┐
│ 去优化  │ → │ 回退解释执行 │ → │ 继续收集   │ → │ 可能重新   │
│ 触发   │   │            │   │ profile   │   │ 编译      │
└────────┘   └────────────┘   └──────────┘   └──────────┘


原因: 投机优化的假设不成立
受影响方法: 被去优化的方法及其调用者 (因内联传播)

被去优化后怎么办?方法回到解释执行,继续收集 profile。如果 profile 稳定了(不再变化),JIT 可能再次编译——这次使用更完整的 profile,做出更准确的假设。

去优化不影响正确性:解释器始终持有字节码的完整语义。编译代码只是"加速器"——加速器坏了(去优化),退回步行走路(解释执行),程序逻辑不受影响。这是 JIT 安全和 C/C++ 未定义行为的本质区别。

三、图解全景

3.1 5 级编译流程图

┌─────────────────────────────────────────────────────────────────────────┐
│                        分层编译完整流程图                                    │
│                                                                         │
│                 ┌──────────────────────┐                                │
│                 │  Level 0: 解释执行    │                                │
│                 │  ┌────────────────┐  │                                │
│                 │  │ 方法调用计数     │  │                                │
│                 │  │ 回边计数        │  │                                │
│                 │  └────────────────┘  │                                │
│                 └──────────┬───────────┘                                │
│                            │                                             │
│             调用计数达阈值    │  回边计数达阈值                              │
│              (CompileThreshold)    (OSR Threshold)                        │
│                            │                                             │
│                  ┌─────────┴──────────┐                                 │
│                  ▼                    ▼                                  │
│          ┌──────────────┐   ┌────────────────┐                          │
│          │ 方法极小?      │   │ Level 3: C1完整 │──→ 方法还在循环中运行时执行OSR│
│          │ (≤8字节)       │   │ profiling       │  ↓                       │
│          └──┬───┬───────┘   └───────┬────────┘  OSR 编译版本(L4)          │
│       是    │   │ 否                │                                     │
│        ▼    │   │                   │              ┌──────────────┐      │
│  ┌────────┐ │   │                   │     ┌──────── │ C2 编译引擎   │      │
│  │Level 1 │ │   │                   │     │         │ (Ideal Graph) │      │
│  │C1 无   │ │   │                   │     │         └──────────────┘      │
│  │profile │ │   │                   │     ▼                               │
│  └────────┘ │   ▼                   │  ┌─────────────────────┐            │
│             │  ┌────────────┐       │  │  Level 4: C2 完整   │            │
│             │  │ Level 2:   │       │  │  优化                │            │
│             │  │ C1 基础    │───────┘  │  使用 L3 的 profile │            │
│             │  │ profiling  │          └──────────┬──────────┘            │
│             │  └────────────┘                     │                        │
│             │                                     │                        │
│             │            ┌────────────────────────┘                        │
│             │            ▼                                                 │
│             │  ┌──────────────────────┐                                   │
│             │  │  去优化 (Deopt)?      │────── 假设失败 → 回 L0             │
│             │  │  - 类加载             │                                   │
│             │  │  - 类型 profile 变化   │                                   │
│             │  │  - 分支 profile 变化   │                                   │
│             │  └──────────────────────┘                                   │
│             │         │ 假设依然成立                                       │
│             │         ▼                                                   │
│             │  ┌──────────────────────┐                                   │
│             │  │ 编译版本持续使用       │                                   │
│             │  │ 直到新的事件触发       │                                   │
│             │  │ 重新编译或去优化       │                                   │
│             │  └──────────────────────┘                                   │
└─────────────────────────────────────────────────────────────────────────┘

3.2 C1 vs C2 优化管线对比

C1 编译管线 (~50ms/方法):
┌──────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────┐    ┌──────────┐
│          │    │              │    │              │    │          │    │          │
│ 字节码   │────│ HIR 构建+优化  │────│ LIR 生成+优化  │────│ 线性扫描   │────│ 机器码    │
│          │    │ (SSA, 常量    │    │ (内存操作,    │    │ 寄存器分配  │    │          │
│          │    │  折叠, DCE)  │    │  窥孔优化)    │    │ (O(n))    │    │          │
│          │    │              │    │              │    │          │    │          │
└──────────┘    └──────────────┘    └──────────────┘    └──────────┘    └──────────┘

C2 编译管线 (~1000ms/方法):
┌──────────┐    ┌──────────────────────────────┐    ┌──────────┐    ┌──────────┐
│          │    │                              │    │          │    │          │
│ 字节码   │────│ Ideal Graph + 多轮优化        │────│ 指令匹配   │────│ 图着色    │
│          │    │ ┌────────────────────────────┐│    │ (SSA→     │    │ 寄存器分配 │
│          │    │ │ 迭代 pass (每次 pass 遍历   ││    │  机器码)  │    │ (O(n²))  │
│          │    │ │ 整个图并转换)              ││    │          │    │          │
│          │    │ └────────────────────────────┘│    │          │    │          │
│          │    │        │ 重新应用优化          │    │          │    │          │
│          │    │        ▼                      │    │          │    │          │
│          │    │  ┌────────────────────────┐   │    │          │    │          │
│          │    │  │ 再次扫描图, 应用更多优化 │   │    │          │    │          │
│          │    │  └────────────────────────┘   │    │          │    │          │
│          │    │        │ 循环至收敛            │    │          │    │          │
│          │    │        ▼                      │    │          │    │          │
│          │    │  图不再变化 → 优化完成         │    │          │    │          │
│          │    └──────────────────────────────┘    └──────────┘    └──────────┘
│          │                                          │
│          │                                          ▼
│          │                                  ┌──────────────┐
│          │                                  │ CodeCache    │
│          │                                  │  ┌───┐ ┌───┐  │
│          │                                  │  │A │ │B │  │
│          │                                  │  └───┘ └───┘  │
│          │                                  └──────────────┘
└──────────┘

四、实战验证

bash
# 查看当前使用的编译器
java -XX:+PrintCompilation -version

# 输出示例:
#     52    1       3       java.lang.String::hashCode (55 bytes)
#     52    2       3       java.util.HashMap::hash (20 bytes)
#     53    3       3       java.lang.String::equals (50 bytes)
#     55    4       1       java.lang.Object::<init> (1 bytes)
#                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                          L1 表示 C1 无 profiling 编译
#     57    5       3       java.lang.String::indexOf (70 bytes)
#     73   12       4       java.lang.String::hashCode (55 bytes)
#                          ^^ L4 表示 C2 编译 → hashCode 从 L3 升级到 L4

# 关键观察:
# - Level 1: 极小方法 (Object::<init> 仅 1 字节)
# - Level 3: 大多数方法首次编译
# - Level 4: 热点方法的最终优化
bash
# 查看内联决策
-XX:+PrintInlining

# 输出示例:
# @ 15   java.lang.String::charAt (25 bytes)   inline (hot)
#   @ 8   java.lang.String::checkBounds (21 bytes)   inline (hot)
#     @ 5   java.lang.String::isLatin1 (15 bytes)   inline
#   @ 14   java.lang.StringUTF16::getChar (12 bytes)   inline
# @ 7   java.lang.String::length (6 bytes)   inline (intrinsic)
# @ 22   java.util.HashMap::hash (20 bytes)   already compiled in a previous transaction
# @ 40   java.lang.String::equals (50 bytes)   not inline (method too big)
#                                                              ^^^^^^^^^^^^^^^^
#                                                              内联失败的原因分析
bash
# 分层编译参数
-XX:+TieredCompilation          # 启用分层编译 (JDK 8 默认开启)
-XX:-TieredCompilation          # 禁用分层编译 → 回退到解释 + C2 两级模式
-XX:Tier0ProfilingInterval=33   # L0 profile 采样间隔
-XX:Tier3MinInvocationThreshold=100   # L0 → L3 的调用阈值
-XX:Tier4MinInvocationThreshold=5000  # L3 → L4 的调用阈值

# 编译线程数
-XX:CICompilerCount=2           # 编译器线程数 (默认: CPU 核数 / 2)
                                # C1 使用更多线程, C2 使用更少线程
                                # 每个 C1/C2 线程独立编译不同的方法

# CodeCache 控制
-XX:InitialCodeCacheSize=240m   # CodeCache 初始大小
-XX:ReservedCodeCacheSize=240m  # CodeCache 最大大小 (JDK 8 默认 240m)
-XX:+PrintCodeCache             # 打印 CodeCache 使用情况
bash
# 去优化演示: 观察类型 profiling 变化
# 运行一个使用接口的代码, 观察 -XX:+PrintCompilation 中的 made not entrant / zombie

java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintDeoptimizationDetails Demo

# 去优化日志中的关键标记:
# made not entrant  ← 编译代码被标记为不可进入 (类加载等导致)
# made zombie       ← 编译代码被标记为僵尸 (等待被卸载)

五、面试视角

追问答案要点
C1 和 C2 编译器的核心区别?C1 编译快 (~50ms/方法)、优化温和 (~100 种); C2 编译慢 (~1s/方法)、优化激进 (~1000 种)。C1 用线性扫描寄存器分配,C2 用图着色。C2 有 Ideal Graph 做全局优化。
分层编译的 5 个级别分别是什么?L0 纯解释; L1 C1 无 profile; L2 C1 基础 profile; L3 C1 完整 profile (为 C2 收集关键数据); L4 C2 完整优化 (使用 L3 的 profile)。
为什么需要去优化 (Deoptimization)?JIT 基于运行时 profiling 做"投机优化"(假设类型、分支模式等不变)。假设不成立时必须回退到解释执行以保证正确性。这是 JIT 安全性的核心保障。
L3 为什么是关键层级?L3 收集完整的类型 profile 和分支 profile,是 C2 做深度优化的"眼睛"。没有 profiling 数据,C2 无法做出精确的投机优化。
C2 的 Ideal Graph (Sea-of-Nodes) 有什么优势?数据流和控制流融合在单一图中,优化 pass 可以全局重排操作。比 C1 的 HIR→LIR 两阶段 IR 更灵活,支持更多跨操作的全局优化。
什么是 made not entrant / zombie?not entrant: 编译代码因新类加载或触发去优化而不再接受新调用。zombie: not entrant 后所有现存调用退出,代码可被 CodeCache 回收。
去优化会影响程序正确性吗?不会。解释器掌握完整语义,编译代码只是"加速器"。去优化后回到解释器执行,完全保证正确性(与 C/C++ 未定义行为有本质区别)。

📚 相关链接

  • **解释执行与编译执行** — 解释执行是分层编译的 L0,也是去优化的回退目标
  • **方法内联和逃逸分析** — C2 最重要的两个优化方法
  • **锁消除与标量替换** — C2 的高级优化,依赖逃逸分析
  • ← 返回 **执行引擎索引**

Knowledge4J — Java 知识库