跳到主要内容

JEP 197:分段代码缓存

概括

将代码缓存划分为不同的段,每个段都包含特定类型的编译代码,以提高性能并支持将来的扩展。

目标

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

非目标

分段代码缓存只是为将来的扩展(例如细粒度锁定)提供基础;它尚未实施任何这些改进。

成功指标

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

动机

编译代码的组织和维护对性能有重大影响。如果代码缓存采取错误的操作,就会出现多个因素导致性能下降的情况。随着分层编译的引入,代码缓存的作用变得更加重要,因为与使用非分层编译相比,已编译代码的数量增加了 2X--4X。分层编译还引入了一种新的编译代码类型:插装编译代码(分析代码)。配置文件代码与非配置文件代码具有不同的属性;一个重要的区别是,经过分析的代码具有预定义的有限生命周期,而未经分析的代码可能永远保留在代码缓存中。

当前的代码高速缓存被优化以处理同类代码,即仅一种类型的编译代码。代码缓存被组织为连续内存块顶部的单个堆数据结构。因此,具有预定义的有限生命周期的分析代码与非分析代码混合在一起,后者可能永远保留在代码缓存中。这会导致不同的性能和设计问题。例如,方法清理器在清理时必须扫描整个代码缓存,即使某些条目从未刷新或包含非方法代码。

描述

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

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

对应的代码堆为:

  • 包含非方法代码的非方法代码堆,例如编译器缓冲区和字节码解释器。该代码类型将永远保留在代码缓存中。

  • 包含经过轻微优化、具有较短生命周期的分析方法的分析代码堆。

  • 未分析的代码堆,包含完全优化的、具有潜在较长生命周期的未分析方法。

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

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

  • -XX:NonProfiledCodeHeapSize:设置包含非分析方法的代码堆的大小(以字节为单位)。

  • -XX:ProfiledCodeHeapSize:设置包含分析方法的代码堆的大小(以字节为单位)。

  • -XX:NonMethodCodeHeapSize:设置包含非方法代码的代码堆的大小(以字节为单位)。

代码缓存的接口和实现适合支持多个代码堆。由于代码缓存是 JVM 的核心组件,因此许多其他组件都会受到这些更改的影响,包括以下组件:

  • 代码缓存清理器:现在仅迭代方法代码堆
  • 分层编译策略:根据代码堆中的可用空间设置编译阈值
  • Java Flight Recorder (JFR):与代码缓存相关的事件
  • 间接引用来自:
    • 可维护性代理:代码缓存内部的 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 启动时非方法代码堆中所需的空间。

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