1. 概述

在本教程中,我们将快速了解一下 JVM 的代码缓存(code cache)内存区域。

2. 什么是代码缓存?

简单来说,JVM 代码缓存是一块用于存放 JVM 将字节码编译为本地机器码后所生成的可执行代码的内存区域。每一块这样的本地可执行代码被称为 nmethod,它可能是一个完整的 Java 方法,也可能是被内联优化后的代码片段。

JIT 编译器是代码缓存最大的使用者,因此有些开发者也称其为 JIT 代码缓存。

3. 代码缓存调优

代码缓存大小是固定的。一旦缓存被填满,JVM 就不会再进行新的编译工作,此时 JIT 编译器将被关闭。你可能会看到类似下面的警告信息:

CodeCache is full. Compiler has been disabled.

这会导致应用性能下降。为了避免这种情况,我们可以通过以下 JVM 参数来调整代码缓存的大小:

  • InitialCodeCacheSize:初始代码缓存大小,默认为 160KB
  • ReservedCodeCacheSize:最大代码缓存大小,默认为 48MB
  • CodeCacheExpansionSize:每次扩展的大小,通常是 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 编译器提前关闭,进而影响响应时间和吞吐量。


原始标题:Introduction to JVM Code Cache