Code Cache 满或碎片化会直接禁用 JIT 编译,所有新方法退至解释执行,性能跌至约 1/10;分段缓存下任一 segment(如 profiled nmethods)满载即导致对应编译器停摆,且解释执行为不可逆降级。

Code Cache 满不是“慢一点”,而是直接切断 JIT 编译通道,所有新方法退回到解释执行——性能通常跌至原来的 1/10。更隐蔽的风险在于:即使总空间没耗尽,碎片化会让 JVM 找不到连续内存块,提前触发编译禁用,造成同样后果。
解释执行不是临时兜底,而是不可逆降级
一旦出现 CodeCache is full. The compiler has been disabled. 日志,JVM 就永久关闭新方法的编译请求。CompilerThread 线程仍在运行,但已不接受任何任务。已编译的老代码照常跑,但以下全部走解释路径:
- 新上线接口的首次高频调用
- Spring AOP 动态生成的代理方法(如 @Transactional、@Cacheable)
- Lambda 表达式和 MethodHandle 生成的类(Jackson 反序列化泛型时高频触发)
- CGLIB 或 Javassist 运行时增强的方法
性能衰减有放大效应,不止是“慢 10 倍”那么简单
解释器本身开销固定,但真实影响来自三类结构性放大:
- 锁竞争陡增:synchronized 方法在解释模式下每次进入都要完整执行 monitorenter 字节码逻辑,无锁消除、粗化等 JIT 优化全失效;多线程争同一把锁时,延迟可能翻 3–5 倍
- 反射与 Lambda 成倍拖慢:Method.invoke() 每次调用需重新解析签名+校验访问权限;LambdaMetafactory 生成的类未编译时,invokedynamic stub 无法生成,每次调用都走慢路径,实测比编译后慢 5–8 倍
- 小方法循环体彻底失优化:getter/setter、工具类纯计算方法本可被 C2 内联+向量化,满 CodeCache 后只能逐字节解释 aload_0、getfield、ireturn,循环展开完全消失
别只看 total used,SegmentedCodeCache 下单段打满就停编译
OpenJDK 11+ 默认启用分段缓存,拆为三个独立 heap:
- profiled nmethods(C2 编译 + 性能采样)
- non-profiled nmethods(C1 编译或不采样 C2)
- non-nmethods(JNI stub、断点桩等)
问题在于:三者互不共享空间。哪怕 total used 只有 70%,只要 profiled nmethods 达到上限,C2 编译器就立即停摆,而 C1 编译仍可继续——你看到 compiled 数还在涨,但关键热点已无法获得深度优化。
验证方式:jstat -printcodecache 查各段 used/max_used;若任一列为 used == max_used,即为该段瓶颈。
碎片化比单纯打满更难察觉,也更危险
真正满载时,jstat -gccapacity 显示 CCSU ≈ CCMX;但碎片化时:
- CCSU 可能仅 85%,但
jcmd显示 largest free blockVM.code_cache - NonProfiledCode / ProfiledCode 使用率差异极大(比如前者 98%、后者 40%),说明分配严重倾斜
- 结合
jstat -gc观察 CCSC−CCSU 差值很小,但 largest free block 极小 → 典型碎片信号
此时 JIT 不会报 “full”,但 failed 编译数持续上涨,compiled 增速明显放缓——它不是拒绝,是反复申请失败后放弃。










