C2编译器通过数据流与控制流分析删除无“可观察行为”的死代码:结果未读取、不参与返回、不触发副作用(I/O、内存写、同步等)即被彻底消除。

这是因为 C2 编译器在执行死代码消除(DCE)时,会通过数据流与控制流联合分析,判定某段计算是否对程序的**可观察行为**产生任何影响。如果结果未被读取、未参与返回值、未触发副作用(如 I/O、内存写入、同步操作等),C2 就会将整段计算逻辑连同相关变量、表达式甚至整个循环直接删除。
关键判断依据:没有“可观察效应”
C2 不关心你“写了什么”,只关心“留下了什么痕迹”。它依据 Java 内存模型和 JVM 规范定义的“可观察行为”来裁决:
- 变量值未被 读取(例如未出现在 return、条件判断、字段赋值、方法调用参数中)
- 计算过程不引发 副作用(如无 volatile 写、无 synchronized 块、无 Unsafe.put、无 JNI 调用)
- 循环体中所有操作都是纯计算,且循环变量未逃逸、终止条件恒真/恒假但无外部依赖
典型基准测试误用场景
很多微基准(如 JMH 之外手写的 for 循环计时)常犯这类错误:
long sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i * i + 2 * i + 1; // 看似密集计算
}
// ❌ 没有使用 sum:没打印、没返回、没存入 volatile 字段、没作为方法返回值
C2 在编译该方法时,发现 sum 的最终值从未被消费,且循环内无任何副作用,于是直接优化为:
// ✅ 编译后等效代码(完全空方法)
实测中,方法执行时间可能趋近于 0ns —— 不是因为快,而是因为“没了”。
C2 的 DCE 实现机制:IGVN 驱动的节点消减
C2 使用 Iterative Global Value Numbering(IGVN) 进行激进的死代码清理:
- 每个计算被建模为图中的一个节点(如
AddNode、MulNode) - 若节点 无输出边(outcnt == 0),即无人使用其结果,且自身无副作用,则被标记为死节点
- 移除该节点后,其输入节点若也失去所有消费者,将被递归移除
- 该过程反复迭代,直到图稳定 —— 最终只剩影响返回值或副作用的路径
如何避免被“删光”?让计算结果“可见”
在基准测试中必须向 JVM 明确传达:“这个结果很重要”。常用方式:
- 返回计算结果(最直接,JVM 必须保留)
- 写入 volatile 字段(强制内存可见性,禁止优化掉写入)
-
作为黑盒消费:用
Blackhole.consumeCPU(...)(JMH 提供)或Thread.onSpinWait()等屏障辅助 -
参与条件分支并影响后续不可省略的操作(例如
if (sum > 0) System.nanoTime();)
注意:仅打印或日志记录通常不够 —— 如果日志框架本身被 JIT 内联且判定为无副作用,仍可能被连带消除。

















