jit编译器只对热点代码(如调用超10000次的方法)将字节码替换为本地机器码,通过分层编译(c1快速优化、c2深度优化)动态升降级,并依赖运行时行为触发优化。

什么是JIT编译器真正干的事?
JIT不是“把Java代码变快”的黑箱,它只对反复执行的代码(也就是热点代码)做一件事:用本地机器码替换原本要解释执行的字节码。解释器启动快但慢,JIT在运行中悄悄补上性能缺口——所以你不会在main方法刚跑第一遍时就看到优化生效。
- 它不编译全部代码,只盯住高频路径:比如一个被调用10000次的方法,或一个循环体执行了上万次的
for块 - 默认阈值是
-XX:CompileThreshold=10000(Server模式),但这个数会随热度衰减动态调整,不是固定不变的“开关” - 编译后代码存在CodeCache里,可通过
-XX:ReservedCodeCacheSize控制大小;填满了会导致后续热点无法编译,反而降速
怎么确认某个方法真被JIT优化了?
别猜,看日志。JVM提供了直接反馈,但默认关闭,得手动打开:
- 加参数:
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining - 输出里出现
calculate() @ 5 inline (hot),说明该方法已被内联且标记为热点 - 注意第3列数字:1=C1编译(快、轻量),4=C2编译(深优化、耗资源),长期服务应关注是否升到C2
- 如果某方法一直没出现在日志里,可能因为太短(
return x + y)被C1直接内联了,也可能因为虚方法调用+多态不确定,JIT不敢动
为什么加了-XX:MaxInlineSize=100反而更慢?
内联不是越大越好。盲目扩大阈值,容易让JIT把本不该内联的大方法硬塞进调用点,带来反效果:
- 代码膨胀:一个300字节的方法被内联10次,就多出3KB机器码,挤占CPU指令缓存(i-cache)
- C2编译压力剧增:大方法触发C2编译更频繁,占用后台线程,拖慢整体响应
- 逃逸分析失效:内联后原本“不逃逸”的对象,可能因上下文扩大而被迫堆分配
- 推荐做法:保持默认
-XX:MaxInlineSize=35,只对明确高频+确定无重写的getter/setter等,用-XX:FreqInlineSize=325特批
分层编译(Tiered Compilation)到底在分什么?
这不是“先C1再C2”的线性流程,而是并行+升降级的动态策略:
立即学习“Java免费学习笔记(深入)”;
- 解释器跑着,C1已在后台把热点方法编译成带基础优化的机器码(比如简单内联、空检查消除)
- C2同时监听这些C1编译后的方法,若发现它们还在高频执行,就重新用深度优化策略再编译一遍
- 如果某方法突然冷下来(比如用户切换功能模块),JIT可能把它从C2降级回C1甚至解释执行,释放CodeCache
- 禁用分层(
-XX:-TieredCompilation)看似“纯粹”,实则让启动期失去C1缓冲,长尾延迟明显上升
最易被忽略的一点:JIT优化永远依赖运行时行为。写死的if (false)分支,哪怕逻辑再重,只要没被执行过,JIT根本看不到它——这也意味着压测必须覆盖真实流量分布,否则上线后才触发编译,首波请求必然卡顿。










