codecache碎片化会导致jit因无法找到连续内存而提前禁用,需通过jcmd vm.code_cache查看各段使用率及最大空闲块,并结合g1gc与usecodecacheflushing协同清理。

CodeCache 碎片化本身不会直接触发 JIT 停止,但会显著抬高“编译失败”概率,尤其在微服务长期运行、类加载频繁的场景下,它让 ReservedCodeCacheSize 的理论容量变得不可靠——你明明还有空闲字节,却因找不到连续块而提前禁用 JIT。
怎么确认当前 CodeCache 是碎片化而非单纯打满
仅看 jstat -compiler 输出的 failed 计数或日志里 CodeCache is full. Compiler has been disabled. 不够。这些只说明“满了”,不区分是真满还是碎片满。
- 用
jcmd <pid> VM.code_cache</pid>查看各段使用率:如果NonNMethodCode、ProfiledCode、NonProfiledCode三段都远未达上限(比如均 95%,大概率是碎片化 - 配合
jstat -gc <pid></pid>观察CCSC(CodeCache Capacity)和CCSU(CodeCache Used)差值很小,但jcmd显示最大可分配块(largest free block)远小于CCSC - CCSU,这就是典型碎片信号 - OpenJDK 11+ 可加
-XX:+PrintCodeCacheStatisticsOnExit,JVM 退出时打印详细分段与空洞分布,适合压测后复盘
为什么 G1GC + -XX:+UseCodeCacheFlushing 是最实用的组合
默认的 Parallel GC 对 CodeCache 几乎无管理能力;CMS 已废弃;ZGC/Shenandoah 虽支持,但 flushing 机制依赖 JVM 内部触发逻辑,G1 是目前唯一被充分验证能协同清理 CodeCache 的 GC。
-
-XX:+UseG1GC启用后,G1 会在并发标记阶段主动扫描并驱逐冷代码(基于调用计数与 age),降低碎片生成速率 -
-XX:+UseCodeCacheFlushing不是“自动扩容”,而是当 CodeCache 使用率 >80% 时,强制触发一次驱逐(evict),优先清理 profiled 但未再调用的方法体,避免等真正打满才动作 - 二者必须同时启用:单独开
UseCodeCacheFlushing在 Parallel GC 下效果极弱;单独用 G1 但不开启 flushing,则冷代码长期滞留,仍会挤占连续空间
-XX:ReservedCodeCacheSize 设多大才算合理
不是越大越好。过大的 CodeCache 会占用进程虚拟内存(VMA),在容器环境下易被 OOMKiller 误杀;且超过 1GB 后,G1 的 flushing 效率明显下降。
- 观察真实负载:用
jcmd <pid> VM.code_cache</pid>每小时采样一次,连续跑 24 小时,取largest free block的最小值,再乘以 1.5~2 倍,就是安全底线 - 微服务常见范围:Spring Boot 应用(含大量 auto-configuration 类)建议从
384m起步;若使用 GraalVM Native Image 或大量反射代理(如 MyBatis Mapper),至少512m - 禁止混用
-XX:CodeCacheSize和-XX:ReservedCodeCacheSize:前者是 soft limit(仅提示),后者才是硬上限;JDK 9+ 已废弃CodeCacheSize,只认ReservedCodeCacheSize
分层编译(TieredCompilation)和碎片化的关系容易被忽略
默认开启的 -XX:+TieredCompilation 会让同一方法经历 C1(快速编译)→ C2(深度优化)两轮编译,产生两份不同大小的本地代码,旧版本 C1 代码不会立即释放,而是标记为可回收——这正是碎片的主要来源之一。
- 对 CPU 密集型微服务(如实时计算、风控引擎),可尝试
-XX:-TieredCompilation+-XX:+UseC2,跳过 C1,减少中间态代码堆积 - 但 Web API 类服务(大量短生命周期请求)反而更依赖 C1 的低延迟编译,关掉后首请求毛刺明显;此时应优先保 flushing + G1,而非关 tiering
- OpenJDK 17+ 支持
-XX:CompileThresholdScaling=0.5降低触发阈值,让 C2 更早介入,缩短 C1 占据时间,间接缓解碎片
真正难处理的是那些动态生成类(如 CGLIB、Javassist、Lombok @Builder)+ 长期不重启的微服务:CodeCache 里的“幽灵方法”既不被调用也不被卸载,只靠 flushing 很难清干净。这种场景下,定期滚动发布比调参更可靠——碎片不是 bug,是 JIT 编译器在有限内存里做权衡的必然副产品。










