热点优化由字节码执行频次触发,非调用次数;HotSpot 以回边计数(如循环跳转)为主,达阈值(默认10000)后JIT编译,I/O密集型代码因执行慢难触发。

热点优化怎么被触发:不是看代码写了多少次,而是看执行了多少次
JIT 的“热点”不是指函数被调用的次数多,而是指某段字节码(如 Java 的 HotSpot 中的 C1/C2 编译阈值)在解释执行过程中被反复执行,达到预设计数器阈值。比如 HotSpot 默认对方法调用计数器(CompileThreshold)设为 10000 次,但实际触发还依赖回边计数(loop back-edge count),也就是循环体内部跳转被执行的次数——这对包含长循环的代码更敏感。
常见错误现象:System.out.println 频繁调用时,看似“热”,但因 I/O 开销大、执行慢,反而不容易触发 JIT;而一个空循环 for (int i = 0; i 很快就进 C1 编译队列。
- 触发条件取决于运行时采样数据,不是静态分析结果
-
-XX:CompileThreshold=1000可调低阈值用于调试,但生产环境不建议 - 方法内联(inlining)是热点优化的关键前置动作,若方法太长或含虚调用(如未去虚化的
invokevirtual),可能直接被 JIT 排除在编译候选之外
逆优化(deoptimization)发生时,JIT 不是在“认错”,而是在“撤退”
逆优化不是因为代码写错了,而是因为 JIT 编译时做的假设被运行时现实打破了。比如:C2 编译器看到某个 instanceof 在前 10 万次都返回 true,就生成只处理该类型的机器码;第 100001 次传入新类型,就必须退回到解释执行,并重新编译(可能走不同路径)。
典型触发场景包括:
- 类加载导致类层次结构变化(如新增子类,破坏了原先的单实现假设)
- 守护内联(guarding inlining)失败:被内联的方法后来被重定义(
Instrumentation.redefineClasses)或动态代理替换 - 逃逸分析结论失效:原先判定对象不逃逸,结果它被发布到线程外或写入静态字段
- 使用
-XX:+PrintDeoptimizationDetails可看到具体原因,例如输出reason=class_check或reason=unstable_if
为什么有些代码永远不被 JIT,或者刚编译完就被逆优化
这不是 JVM “抽风”,而是编译策略与运行时约束共同作用的结果。比如:java.lang.String::hashCode 在 JDK 7+ 被标记为 @HotSpotIntrinsicCandidate,JIT 直接替换成汇编级实现;但如果你用反射反复调用它,JIT 就无法稳定推测调用目标,大概率不编译或很快逆优化。
- 频繁 GC 会导致编译队列积压,
CompileQueueSize达到上限后新方法直接跳过编译 - 使用
-XX:+TieredStopAtLevel=1强制只用 C1,会禁用大部分激进优化,也就基本不会触发逆优化——但性能也上不去 - 调试模式(
-agentlib:jdwp)下,JIT 默认关闭,所有代码纯解释执行,自然没有热点也没有逆优化
观察和验证的最小可行手段
别靠猜,用 JVM 自带工具看真实行为。启动参数加 -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining,就能在控制台看到每行类似:
123 13 3 java.lang.String::hashCode (67 bytes) 145 22 4 java.util.ArrayList::get (6 bytes) inline (hot)
其中 inline (hot) 表示该方法被内联且触发了热点编译;如果之后出现:
189 2 deopt 13 java.lang.String::hashCode @ 12 reason=unstable_if
说明已逆优化。注意:这些日志本身有性能开销,线上慎用。
真正容易被忽略的是——逆优化不是一次性的。同一个方法可能反复编译、逆优化、再编译,尤其在混合使用反射、动态代理、字节码增强(如 Spring AOP、Mockito)的系统里,这种震荡会持续发生,且难以通过调参彻底消除。

















