codecache是jvm中专用于存放jit编译生成的native code(如x86_64/aarch64机器码)的堆外内存区域,包含non-profiled nmethods、profiled nmethods、adapters和non-nmethods四类内容,不存字节码、类元数据或jni本体,仅存其轻量桩;jdk 11+默认分段管理,需按段排查满载问题。

CodeCache 存放 JIT 编译后的 native code
OpenJDK 的 CodeCache 是一块专用的 JVM 内存区域,**只存编译器输出的机器码**,不存 Java 字节码、类元数据、字符串或任何堆对象。它本质是 JIT 编译器的“成果仓库”——每次 C1 或 C2 编译器把一个热点方法(比如被调用超 -XX:CompileThreshold=10000 次的方法)编译成 x86_64 或 AArch64 指令后,生成的二进制代码就写进这里。
具体包括:
-
non-profiled nmethods:未启用性能分析(-XX:-ProfileInterpreter)时生成的编译方法,如普通业务方法、工具类静态方法 -
profiled nmethods:启用了分析后生成的带分支/调用频率信息的编译方法,用于进一步优化(如内联决策、去虚拟化) -
adapters:连接解释器与编译代码的胶水代码,比如CompiledICstub、MethodHandle适配器、Lambda 生成的invokedynamicstub -
non-nmethods:少量元数据,如断点桩(breakpoint stub)、调试入口等,占比极小
CodeCache 不存放 JNI 代码本体,但存放 JNI 方法桩
JNI 函数本身(即你写的 C/C++ 实现)加载在操作系统原生内存(如 mmap 区),不在 JVM 堆或 CodeCache 中。但 JVM 会在 CodeCache 里为每个注册的 JNI 方法生成一个轻量级桩(stub),负责参数搬运、线程状态切换、异常检查等胶水逻辑。这些 stub 很小,但数量多时(如大量 native 方法 + 动态代理)也会挤占空间。
注意:CodeCache 和 Metaspace 完全无关——前者是执行代码,后者是类元数据;也和 StringTable 或运行时常量池无关,那些都在堆或元空间里。
JDK 8 和 JDK 11+ 的 CodeCache 结构差异直接影响排查方式
JDK 8 默认单段式 CodeCache,所有内容混在一个 heap 里;JDK 11+ 默认开启 -XX:+SegmentedCodeCache,拆成三个独立 heap(non-profiled nmethods、profiled nmethods、non-nmethods)。这意味着:
- JDK 8 报
CodeCache is full,基本就是整体耗尽,调大-XX:ReservedCodeCacheSize是主要解法 - JDK 11+ 可能只有一段满(比如
profiled nmethods满了而其他还有空),此时单纯加总大小没用,得看jstat -printcodecache输出各段的used == max_used状态 -
-XX:+UseCodeCacheFlushing在 JDK 11+ 上对profiled段更有效,但清理有开销,且无法回收已编译但不再使用的 lambda stub
动态生成代码是 CodeCache 的隐形消耗大户
很多开发者只想到“自己写的类多”,其实更危险的是框架自动生成的代码:
- Spring AOP 的 CGLIB 代理类,每个切面组合都生成新
nmethod - Jackson 反序列化泛型类型树时,为每种类型组合生成专用解析器(
BeanDeserializer)并编译 - Lambda 表达式在首次调用时由
LambdaMetafactory生成invokedynamicstub,并触发 JIT 编译 - 字节码增强库(如 Byte Buddy、ASM)在运行时生成的类,只要被 JIT 编译,就占
CodeCache
这些代码往往生命周期短、复用率低,但编译后就常驻 CodeCache 直到 JVM 重启——不像堆对象可被 GC 回收。这也是为什么有些服务跑几天后才突然报警,跟类加载节奏强相关。
别只盯着 ReservedCodeCacheSize 调大,先确认是不是某段 heap 卡死、有没有大量短命动态代码在反复注册编译。否则再大的值也撑不过一次灰度发布。











