热点代码与编译优化
HotSpot JVM 的名字就来自热点代码优化。它不会平均对待所有代码,而是把编译和优化资源集中在最常执行的代码上。
# 1. 什么是热点代码
热点代码是运行中被频繁执行的代码。
常见形式:
- 被频繁调用的方法。
- 执行次数很多的循环体。
- 高频业务路径。
JVM 会通过计数器和运行时 profiling 识别热点。
# 2. 方法内联
方法内联是最重要的 JIT 优化之一。
int result = add(a, b);
如果 add 足够简单,JIT 可能把方法体直接展开到调用点。
优点:
- 减少方法调用开销。
- 暴露更多优化机会。
- 支持后续常量传播、死代码消除。
# 3. 去虚拟化
Java 方法调用通常支持多态。JIT 可以根据运行时类型信息,把虚方法调用优化为直接调用。
例如某接口实际只有一个实现类被频繁使用,JIT 可以基于这个假设优化调用。
如果后来加载了新的实现类,可能触发反优化。
# 4. 常量传播
如果某个值在运行时可以确定,JIT 可以把它传播到后续表达式中。
if (DEBUG) {
log();
}
如果 DEBUG 恒为 false,相关代码可能被优化掉。
# 5. 死代码消除
不会影响结果的代码可能被消除。
int x = compute();
// x 没有被使用
如果 compute() 没有副作用,JIT 可能消除这段计算。
这也是写微基准测试时容易测错的原因:看起来在执行,实际可能被 JIT 优化掉。
# 6. 循环优化
JIT 会对循环做很多优化:
- 循环展开。
- 范围检查消除。
- 循环不变量外提。
- 向量化。
这些优化对数组处理、数值计算、高频循环很重要。
# 7. 微基准测试注意点
不要用普通 main 方法随手测性能。
原因:
- JIT 预热不足。
- 死代码被消除。
- 常量折叠。
- GC 干扰。
- CPU 缓存影响。
Java 微基准测试应使用 JMH。
# 8. 阶段小结
JIT 优化的核心是利用运行时信息优化热点路径。方法内联、去虚拟化、常量传播、死代码消除和循环优化共同提升性能。理解这些优化,也能避免写出错误的性能测试。
# 9. 热点探测数据结构
JVM 会维护一些计数信息:
Method
├─ invocation counter 方法调用次数
├─ backedge counter 循环回边次数
└─ profiling data 类型分布、分支概率等
当计数超过阈值,方法或循环就可能进入编译队列。
# 10. 方法内联为什么重要
内联前:
caller()
└─ call add()
└─ return a + b
内联后:
caller()
└─ 直接执行 a + b
内联带来的收益不只是少一次方法调用,更重要的是打开后续优化空间:
- 常量传播。
- 死代码消除。
- 逃逸分析。
- 锁消除。
- 循环优化。
# 11. 微基准错误示例
错误写法:
public static void main(String[] args) {
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
new Object();
}
System.out.println(System.nanoTime() - start);
}
问题:
- 对象可能被消除。
- 没有预热。
- 循环可能被优化。
- GC 和系统噪声没有控制。
正确方向是使用 JMH,并通过 Blackhole 消耗结果。
# 专家速查表
| 优化手段 | 作用 | 失效条件 |
|---|---|---|
| 方法内联 | 减少调用开销并扩大优化范围 | 方法过大、多态过强 |
| 逃逸分析 | 判断对象是否逃出作用域 | 对象被返回或共享 |
| 标量替换 | 拆散对象字段 | 逃逸分析失败 |
| 循环优化 | 降低循环成本 | 分支复杂或边界不稳定 |
| 去虚拟化 | 把虚调用变直接调用 | 类型分布变化 |
热点优化不是源码里写了某个模式就必然发生。JIT 会根据调用频率、类型分布、方法大小、逃逸情况和运行时反馈动态决定。
# 12. Tips 快问快答
Q:方法越小越容易内联吗?
A:通常是,但不是唯一条件。调用频率、字节码大小、类型稳定性等都会影响。
Q:为什么反射调用可能慢?
A:反射破坏部分静态类型信息,JIT 优化空间较小。现代 JDK 对反射也有优化,但普通直接调用仍更友好。
Q:JIT 会不会改变程序语义?
A:不会。JIT 优化必须保证在 Java 内存模型和语言语义下结果一致。
Q:热点代码是开发者手动标记的吗?
A:不是。JVM 根据运行时计数和 profiling 自动识别。