跳到主要内容

JEP 197:分段代码缓存

QWen Max 中英对照

概述

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

目标

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

非目标

分段代码缓存仅为诸如细粒度锁定等未来扩展提供了一个基础;它尚未实现任何这些改进。

成功指标

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

动机

编译代码的组织和维护对性能有着显著的影响。如果代码缓存采取了错误的操作,已有报告指出性能回归会达到数倍之多。随着分层编译(tiered compilation)的引入,代码缓存的角色变得更加重要,因为与非分层编译相比,编译代码的数量增加了 2 倍到 4 倍。分层编译还引入了一种新的编译代码类型:检测编译代码(profiled code)。检测代码与非检测代码有不同的特性;一个重要的区别是,检测代码具有预定义的有限生命周期,而非检测代码则可能永远保留在代码缓存中。

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

描述

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

  • JVM 内部(非方法)代码
  • 已分析代码
  • 未分析代码

对应的代码堆是:

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

  • 一个包含轻度优化、带有性能分析的方法的代码堆,这些方法的生命周期较短。

  • 一个包含完全优化、未进行性能分析的方法的代码堆,这些方法可能具有较长的生命周期。

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

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

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

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

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

代码缓存的接口和实现被调整为支持多个代码堆。由于代码缓存是 JVM 的核心组件,因此许多其他组件也受到这些更改的影响,包括以下内容:

  • 代码缓存清理器:现在仅遍历方法代码堆
  • 分层编译策略:根据代码堆中的可用空间设置编译阈值
  • Java 飞行记录器 (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。

风险与假设

为每个代码堆(code heap)固定大小会导致潜在的内存浪费,即当一个代码堆已满而另一个代码堆仍有空间时。特别是对于非常小的代码缓存(code cache)大小,可能会出现编译器被关闭的情况,即使仍有可用空间。为了解决这个问题,将添加一个选项,用于在代码缓存较小时关闭分段功能。

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

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