CodeCache满后JIT立即停摆且不可逆,所有未编译代码退至解释执行,性能降至约1/10;已编译代码仍运行,但新增方法、代理、Lambda等全走低效解释路径,需用jstat -compiler和-printcodecache定位segment级满载问题。

CodeCache 满后 JIT 立即停摆,解释器被迫扛压
当 CodeCache 耗尽,JVM 不会“慢慢降级”,而是直接触发 Compiler has been disabled 日志,并永久关闭 JIT 编译线程(如 CompilerThread0)。此后所有尚未编译的方法,包括新加载的类、动态生成的代理、Lambda 重写的方法,全部退回到纯解释执行模式。这不是临时策略,而是一次性不可逆的降级——除非重启 JVM。
关键点在于:已编译的代码仍可高速运行,但新增/未编译路径全走解释器,而解释器执行效率通常只有 JIT 编译代码的 1/10 左右。这种衰减在高并发、高频调用新方法的场景(如 Spring AOP 动态代理链、Jackson 反序列化泛型类型树)中尤为剧烈。
用 jstat -compiler 和 -printcodecache 定位解释器负载飙升
单靠 GC 日志或 CPU 使用率无法区分是 GC 压力还是解释器拖慢,必须用 JVM 自带诊断工具确认编译状态是否中断:
-
jstat -compiler <pid></pid>:观察failed列是否持续增长,compiled是否停滞;若total不再增加且failed非零,基本可判定 JIT 已停 -
jstat -printcodecache <pid></pid>(JDK 11+):输出各 CodeHeap(non-profiled nmethods、profiled nmethods)的used与max_used;若任一 heap 的used == max_used,即为满载 - 配合
-XX:+PrintCodeCacheOnCompilation启动时启用,可在日志中看到每次编译前的剩余空间,快速定位耗尽发生时刻
解释执行性能衰减不是线性的,而是有拐点的
解释器本身开销固定,但真实衰减来自三类放大效应:
-
方法内联失效:JIT 的
-XX:MaxInlineSize和-XX:MaxInlineLevel在解释模式下完全不生效,导致深度调用链(如 Stream pipeline、递归 JSON 解析)反复查表跳转 - 锁竞争加剧:解释器对 synchronized 方法、volatile 字段访问的处理比 JIT 编译后更重,尤其在多线程高频访问同一对象时,
monitorenter解释开销成倍放大 -
反射与 Lambda 代价翻倍:
Method.invoke()、LambdaMetafactory生成的类在未编译状态下需每次解析字节码签名,比已编译的 invokedynamic stub 慢 5–8 倍
别只盯着 ReservedCodeCacheSize,SegmentedCodeCache 才是隐性瓶颈
OpenJDK 11+ 默认启用 -XX:+SegmentedCodeCache,将 CodeCache 拆为三个独立 heap:non-profiled nmethods、profiled nmethods、non-nmethods(存 JNI stub 等)。问题在于:即使总空间充足,某个 segment(尤其是 profiled nmethods)也可能先满,导致对应编译器(C1 或 C2)单独停摆,而其他 segment 还有大量空闲。
验证方式:jinfo -flag SegmentedCodeCache <pid></pid> 查是否启用;再用 jstat -printcodecache 分别看三块使用率。常见误判是看到 total used = 180M / 240M 就认为安全,实则 profiled nmethods 已 100% 占用——此时 C2 编译器已静默退出,只剩 C1 或纯解释器在撑。
修复建议:-XX:-SegmentedCodeCache 强制回退到统一 heap(适合中小应用),或针对性调大最易满的 segment,例如 -XX:NonProfiledCodeHeapSize=120m -XX:ProfiledCodeHeapSize=80m(需 JDK 12+)。


















