先说结论

JVM 先以解释或较低优化层级快速启动,通过调用计数和回边计数识别热点,再把热点方法编译成本地代码。JIT 可根据运行时类型信息做内联、常量折叠、锁消除、标量替换和去虚拟化,并在假设失效时去优化。

分层编译在启动速度与峰值性能间折中,具体编译器和阈值由 JVM 实现与配置决定。

关键优化

方法内联扩大后续优化范围;逃逸分析判断对象是否离开方法或线程,从而可能消除分配和锁;去虚拟化根据热点类型把动态调用转成更直接路径,并保留假设失效时回退能力。

从解释到优化编译

应用刚启动时,解释器能快速执行字节码并收集方法调用、循环回边和类型画像。热点达到阈值后,分层编译器会先生成较快的代码,再在拥有更多运行时信息时进行更激进优化。这样避免所有代码一开始都花时间编译,也能让冷代码保持低成本。

热点不是只看方法被调用次数。长循环即使只调用一次也可能因为回边计数成为优化对象;反之一个看似频繁的小方法若总被内联,单独编译价值可能很低。

内联为何是核心

int price(Order order) {
    return discount.apply(order.total());
}

若 JIT 观察到 discount 在热点路径上总是某个实现,它可内联 apply,继而做常量传播、分支消除和标量替换。没有内联,调用边界会阻止很多跨方法优化。方法过大、调用点多态过强、递归或编译预算限制都可能让内联失败。

推测优化与去优化

JIT 会基于“当前只看到一种类型”“某分支几乎不走”等画像做推测。若后来出现新子类、类卸载或罕见分支,编译代码的假设失效,JVM 可回退到解释执行或重新编译,这称为去优化。它是动态语言性能的正常机制,不必把每次 deoptimization 都视为故障。

频繁去优化才值得分析,常见原因包括类型分布剧烈变化、热代码不断加载新类或基准测试没有稳定预热。

如何做可信基准

预热足够轮次 -> 测量多次 -> 防止结果未使用被消除
        -> 隔离 GC/JIT 噪声 -> 报告分位数与环境

JMH 能处理 fork、预热、黑洞消费和统计,适合微基准。业务性能还必须用真实数据、线程竞争、I/O 和 GC 压力验证;一个方法的 ns/op 改善不等于接口 P99 改善。

可观测手段

可通过 JFR、JIT 编译日志、火焰图和反汇编工具观察热点与编译活动。启用详细编译日志会有开销,生产上优先短时、受控采集。优化前先确认 CPU 真花在目标代码上,避免为了“让 JIT 更容易优化”牺牲可读性却没有收益。

测量边界

微基准必须预热、防止死代码消除并隔离 JVM 噪声,通常使用 JMH。一次 System.nanoTime 包围循环很容易测到编译过程、常量折叠或完全被删除的代码。

容易踩坑的地方

源码看起来创建对象不代表运行时一定分配,反之也不能保证逃逸分析必然优化。JIT 优化依赖实际热点、类型分布和代码形态,不应靠猜测替代性能剖析。

常见问题

追问:为什么 Java 应用刚启动时较慢?

类加载、初始化、缓存建立和热点代码逐步编译都会影响预热期;生产系统可通过预热流量、合理探针和观察稳态指标降低影响。

追问:final 方法一定会被内联吗?

final 有助于静态分析,但是否内联仍取决于热点、方法大小、调用频率和编译预算。非 final 的单态调用点也可能被去虚拟化并内联。

追问:为什么循环里创建对象有时并不慢?

短命对象可在 TLAB 中快速分配并由年轻代高效回收,甚至被标量替换消除。真正成本要看分配率、逃逸、对象大小和 GC 结果。

追问:可以通过 JVM 参数强制所有方法编译吗?

存在相关诊断和编译控制参数,但强制编译会增加启动时间与代码缓存压力,且绕过 JIT 的画像决策。除诊断外不宜作为常规优化手段。