1. 概述
在本教程中,我们将快速了解一下 JVM 的代码缓存(code cache)内存区域。
2. 什么是代码缓存?
简单来说,JVM 代码缓存是一块用于存放 JVM 将字节码编译为本地机器码后所生成的可执行代码的内存区域。每一块这样的本地可执行代码被称为 nmethod,它可能是一个完整的 Java 方法,也可能是被内联优化后的代码片段。
JIT 编译器是代码缓存最大的使用者,因此有些开发者也称其为 JIT 代码缓存。
3. 代码缓存调优
✅ 代码缓存大小是固定的。一旦缓存被填满,JVM 就不会再进行新的编译工作,此时 JIT 编译器将被关闭。你可能会看到类似下面的警告信息:
CodeCache is full. Compiler has been disabled.
这会导致应用性能下降。为了避免这种情况,我们可以通过以下 JVM 参数来调整代码缓存的大小:
InitialCodeCacheSize
:初始代码缓存大小,默认为 160KBReservedCodeCacheSize
:最大代码缓存大小,默认为 48MBCodeCacheExpansionSize
:每次扩展的大小,通常是 32KB 或 64KB
增大 ReservedCodeCacheSize
是一种解决方案,但通常只是临时缓解问题。
幸运的是,JVM 提供了一个参数 UseCodeCacheFlushing
来控制是否启用代码缓存清理机制,默认值是 false
。当我们启用它时,在满足以下条件时会自动清理缓存中的部分代码:
- ✅ 代码缓存已满,并且使用量超过某个阈值
- ✅ 距离上次清理已经过了一定时间间隔
- ✅ 预编译的代码“热度”不够高(JVM 会对每个方法维护一个热度计数器)
如果某个方法的热度低于设定阈值,就会被回收以腾出空间。
4. 监控代码缓存使用情况
为了监控代码缓存的使用状况,我们需要关注当前已使用的内存大小。
我们可以在启动 JVM 时添加如下参数:
-XX:+PrintCodeCache
运行程序后,可以看到类似输出:
CodeCache: size=32768Kb used=542Kb max_used=542Kb free=32226Kb
各字段含义如下:
size
:代码缓存的最大容量,等同于ReservedCodeCacheSize
used
:当前实际使用的大小max_used
:历史上使用过的最大值free
:尚未使用的剩余空间
📌 使用 PrintCodeCache
可以帮助我们:
- 观察何时发生了缓存清理操作
- 判断是否接近内存使用的临界点
5. 分段代码缓存(Segmented Code Cache)
⚠️ 从 Java 9 开始,JVM 将代码缓存划分为三个独立的段(segment),每个段存储特定类型的编译代码。分别是:
Non-method 段:
- 包含 JVM 内部相关代码,如字节码解释器
- 默认大小约为 5MB
- 可通过
-XX:NonNMethodCodeHeapSize
调整大小
Profiled-code 段:
- 存放轻度优化、生命周期可能较短的代码
- 默认大小约 122MB
- 可通过
-XX:ProfiledCodeHeapSize
进行配置
Non-profiled 段:
- 存放完全优化、可能长期存活的代码
- 默认大小约 122MB
- 可通过
-XX:NonProfiledCodeHeapSize
设置大小
✅ 这种分段结构使得不同类型的编译代码能够区别对待,从而提升整体性能。
例如,将短期和长期代码分开可以提高方法清理器(method sweeper)的工作效率,因为它只需要扫描更小的内存范围。
6. 总结
本文简要介绍了 JVM 中的代码缓存机制,并展示了如何通过 JVM 参数对其进行监控和调优。
对于追求极致性能的系统来说,合理配置代码缓存大小以及启用清理策略是非常有必要的。特别是在长时间运行的服务中,忽略这一点可能导致 JIT 编译器提前关闭,进而影响响应时间和吞吐量。