JVM通过方法调用计数器(默认10000次)和回边计数器(默认100000次)识别热点,触发C1或C2分层编译;计数器支持衰减,OSR编译可替换栈上循环代码;需配合-XX:+PrintCompilation等参数验证。

JVM 利用基于计数器的热点探测(Counter-based Detection)识别高频方法,并驱动 JIT 编译器将其编译为本地机器码,整个过程是自动、分阶段、且依赖运行时统计的。它不靠人工干预,而是由 HotSpot 虚拟机在解释执行过程中实时维护两类计数器,达到阈值后触发编译决策。
方法调用计数器决定“谁该编译”
JVM 为每个非静态、非私有(或虽私有但可内联)的方法维护一个调用计数器。每次方法被调用,计数器加一。默认阈值为 10000 次(Server 模式),一旦达标,该方法就会被提交给 C1 编译器进行首次编译。
- 这个计数器只对“实际进入方法体”的调用生效(不包括已被内联的调用)
- final、static、private 方法也可能被编译,但虚方法(如普通实例方法)更易参与热度统计和后续优化
- 计数器支持衰减(-XX:+UseCounterDecay,默认开启),长时间不调用会逐步归零,避免无效方法长期占用 Code Cache
回边计数器捕获“循环级热点”
针对方法内部的高频循环(如 for、while),JVM 统计“回边指令”执行次数——即每次循环条件满足后跳转回循环头的次数。默认阈值为 100000 次(约为调用阈值的 10 倍)。
- 即使方法总调用次数不高,只要循环体执行极频繁,也会触发 OSR(On-Stack Replacement)编译:JVM 在栈上直接替换正在解释执行的循环代码为已编译的本地版本
- OSR 编译对象仍是整个方法,但入口点从循环头开始,不是方法起始字节码
编译不是一步到位,而是分层升级
计数器达标只是起点,后续是否升级到深度优化,取决于运行时 Profile 数据:
- C1 编译(Tier 1):快速生成带基础优化(如方法内联、空检查消除)的本地代码,延迟低,适合启动初期
- C2 编译(Tier 4):需持续高频 + 类型稳定 + 无去优化事件,才触发;做逃逸分析、循环展开、冗余分支裁剪等激进优化
- 可通过 -XX:+PrintCompilation 观察编译级别:日志中第三列数字为 1/2 表示 C1,3/4 表示 C2
验证与调优的关键手段
想确认某方法是否因计数器触发编译,不能只看调用次数,还要保障 JVM 有足够时间完成编译流程:
- 加入 Thread.sleep(5000) 或持续调用,避免 main 方法退出太快导致 JIT 线程来不及响应
- 启动参数加上 -XX:+PrintCompilation,观察输出是否含目标方法名及编译级别
- 如需调试,可用 -XX:CompileThreshold=1000 降低阈值(仅限测试环境),或 -XX:+PrintTieredEvents 查看各层级计数器变化细节
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











