Skip to content

JEP 197: Segmented Code Cache | 分段代码缓存

摘要

将代码缓存分成不同的段,每个段包含特定类型的编译代码,以提高性能并实现未来的扩展。

目标

  • 分离非方法代码、已配置文件的代码和未配置文件的代码
  • 通过跳过非方法代码的专用迭代器,缩短扫描时间
  • 提高某些编译密集型基准测试的执行时间
  • 更好地控制 JVM 内存占用
  • 减少高度优化代码的碎片化
  • 改善代码局部性,因为相同类型的代码可能在时间上紧密访问
    • 更好的 iTLB 和 iCache 行为
  • 为未来的扩展奠定基础
    • 改进异构代码的管理,例如 Sumatra(GPU 代码)和 AOT 编译代码
    • 可能实现每个代码堆的细粒度锁定
    • 将代码和元数据分离的可能性(参见 JDK-7072317)

非目标

分段的代码缓存只是为未来的扩展提供了基础,比如细粒度锁定;目前尚未实现任何这些改进。

成功指标

  • 不同代码类型的分离
  • 较短的扫描时间
  • 较低的执行时间
  • 减少高度优化代码的碎片化
  • 减少 iTLB 和 iCache 未命中的次数

动机

编译代码的组织和维护对性能有重要影响。如果代码缓存采取错误的操作,可能会导致多个因素的性能回归。随着分层编译的引入,代码缓存的作用变得更加重要,因为编译代码的数量比非分层编译增加了 2 倍至 4 倍。分层编译还引入了一种新的编译代码类型:配置文件编译代码(已配置文件的代码)。已配置文件的代码与非配置文件的代码有不同的属性;一个重要的区别是已配置文件的代码具有预定义的有限生命周期,而非配置文件的代码可能永远存在于代码缓存中。

当前的代码缓存被优化为处理同质代码,即只有一种类型的编译代码。代码缓存被组织为一个位于连续内存块之上的单个堆数据结构。因此,具有预定义有限生命周期的已配置文件的代码与可能永远存在于代码缓存中的非配置文件的代码混合在一起。这导致了不同的性能和设计问题。例如,方法清除器在扫描时必须扫描整个代码缓存,即使某些条目从不被清除或包含非方法代码。

描述

代码缓存不再是一个单一的代码堆,而是被分成不同的代码堆,每个堆包含特定类型的编译代码。这样的设计使我们能够分离具有不同属性的代码。有三种不同的顶级编译代码类型:

  • JVM 内部(非方法)代码
  • 已配置文件的代码
  • 未配置文件的代码

对应的代码堆为:

  • 一个非方法代码堆,包含非方法代码,例如编译器缓冲区和字节码解释器。这种代码类型将永远存在于代码缓存中。
  • 一个已配置文件的代码堆,包含轻度优化的、已配置文件的方法,寿命较短。
  • 一个未配置文件的代码堆,包含完全优化的、未配置文件的方法,寿命可能较长。

非方法代码堆的大小固定为 3MB,用于 VM 内部以及编译器缓冲区的额外空间。这个额外空间根据 C1/C2 编译器线程的数量进行调整。剩余的代码缓存空间均匀分布在已配置文件和未配置文件的代码堆之间。

引入以下命令行开关来控制代码堆的大小:

  • -XX:NonProfiledCodeHeapSize:设置包含未配置文件方法的代码堆的大小(以字节为单位)。
  • -XX:ProfiledCodeHeapSize:设置包含已配置文件方法的代码堆的大小(以字节为单位)。
  • -XX:NonMethodCodeHeapSize:设置包含非方法代码的代码堆的大小(以字节为单位)。

代码缓存的接口和实现被调整以支持多个代码堆。由于代码缓存是 JVM 的一个核心组件,这些变化会影响到许多其他组件,包括以下内容:

  • 代码缓存清除器:现在只迭代方法代码堆
  • 分层编译策略:根据代码堆中的空闲空间设置编译阈值
  • Java Flight Recorder(JFR):与代码缓存相关的事件
  • 间接引用来自:
    • Serviceability Agent:用于访问代码缓存内部的 Java 接口
    • DTrace ustack 辅助脚本(jhelper.d):解析已编译 Java 方法的名称
    • Pstack 支持库(libjvm_db.c):跟踪已编译的 Java 方法的堆栈

备选方案

另一种实现方式是定义逻辑内存区域,将不同类型的代码优先分配到这些区域。如果有空闲空间,我们将分配到首选内存区域,如果没有剩余空间,我们将分配到其他地方。

测试

使用 JPRT、Nashorn + Octane、SPECjbb2013、SPECjbb2005、SPECjvm2008 进行密集的正确性测试。

我们需要确保没有性能下降,尤其是对于代码缓存大小较小的嵌入式使用情况。

测试受影响的组件,包括 Serviceability Agent、DTrace、Pstack、Java Flight Recorder。

风险和假设

每个代码堆的固定大小可能导致内存浪费,如果一个代码堆已满,而另一个代码堆仍有空间。特别是对于非常小的代码缓存大小,即使还有空间可用,编译器也可能被关闭。为解决这个问题,将添加一个选项来关闭小型代码缓存的分段。

非方法代码的大小取决于 Java 应用程序、底层平台和 JVM 设置。因此,很难确定 JVM 启动时非方法代码堆所需的空间。

该补丁的未来版本可能会实现动态调整大小(由清除器支持)或不同的分配策略,以降低浪费内存的风险。