Skip to content

JEP 352: Non-Volatile Mapped Byte Buffers | 非易失性映射字节缓冲区

摘要

添加新的特定于 JDK 的文件映射模式,以便 FileChannel API 可用于创建引用非易失性内存(NVM)的 MappedByteBuffer 实例。

目标

本 JEP 提议将 MappedByteBuffer 升级为支持访问非易失性内存(NVM)。所需的唯一 API 更改是 FileChannel 客户端使用的新枚举,用于请求位于 NVM 支持的文件系统上的文件映射,而不是传统的文件存储系统。MappedByteBuffer API 的最近更改意味着它支持所有必要的行为,以允许直接内存更新并提供高级 Java 客户端库实现持久数据类型(例如块文件系统、日志记录、持久对象等)所需的持久性保证。需要修订 FileChannelMappedByteBuffer 的实现以了解映射文件的这种新型支持类型。

本 JEP 的主要目标是确保客户端可以从 Java 程序中高效且一致地访问和更新 NVM。该目标的一个关键要素是确保对缓冲区区域的单个写入(或少量连续写入)可以以最小的开销提交,即确保任何可能仍在缓存中的更改都被写回内存。

第二个次要目标是使用在 Unsafe 类中定义的受限 JDK 内部 API 来实现此提交行为,允许除 MappedByteBuffer 之外的其他可能需要提交 NVM 的类重用它。

最后一个相关的目标是允许通过现有监视和管理 API 跟踪映射在 NVM 上的缓冲区。

注意

目前已经可以将 NVM 设备文件映射到 MappedByteBuffer 并使用当前的 force() 方法提交写入操作,例如使用 Intel 的 libpmem 库作为设备驱动程序,或者将 libpmem 作为本地库进行调用。然而,使用当前 API,这两种实现都提供了“一锤子买卖”的解决方案。force 方法无法区分干净和脏行的差异,并且需要系统调用或 JNI 调用来实现每个写回操作。由于这两个原因,现有功能无法满足本 JEP 的效率要求。

本 JEP 的目标操作系统 /CPU 平台组合是 Linux/x64 和 Linux/AArch64。这一限制有两个原因。首先,该功能仅适用于支持 mmap 系统调用 MAP_SYNC 标志的操作系统,该标志允许同步映射非易失性内存。这是最近的 Linux 版本所支持的。其次,它也只能在支持在用户空间控制下缓存行写回的 CPU 上运行。x64 和 AArch64 都提供了满足这一要求的指令。

非目标

本 JEP 的目标并不超出为 NVM 提供访问和持久性保证的范围。特别是,本 JEP 并不旨在为其他重要行为(如 NVM 的原子更新、读写器的隔离或独立持久内存状态的一致性)提供支持。

最近的 Windows/x64 版本确实支持 mmap 的 MAP_SYNC 标志。但是,为该操作系统 /CPU 组合(或任何其他可能的平台)提供此功能的目标将推迟到后续更新中。

成功指标

效率目标很难精确量化。但是,将数据持久化到内存的成本应该相对于两种现有替代方案显著降低。首先,它应该显著减少将数据同步写入传统文件存储的成本,即包括确保单个写入操作必定命中磁盘所需的常规延迟。其次,成本也应该显著低于使用基于驱动程序的解决方案(如 libpmem)通过系统调用写入 NVM 的成本。合理预期成本将相对于同步文件写入降低一个数量级,相对于使用系统调用降低两倍。

动机

NVM 为应用程序编程人员提供了在程序运行之间创建和更新程序状态的机会,而不会像通常从持久介质输出和输入时那样产生显著的复制和 / 或转换成本。这对于需要定期持久保存有疑问状态以启用崩溃恢复的事务性程序来说尤为重要。

现有的 C 库(如 Intel 的 libpmem)为 C 程序提供了在基础级别上高效访问 NVM 的功能。它们还基于这一点来支持简单管理各种持久性数据类型。目前,即使在 Java 中仅使用基础库也是昂贵的,因为经常需要进行系统调用或 JNI 调用来调用确保内存更改持久的原始操作。同样的问题限制了更高级别库的使用,而且由于 C 中提供的持久数据类型是在 Java 无法直接访问的内存中分配的,这一问题进一步加剧。这使得 Java 应用程序和中间件(例如,Java 事务管理器)相对于 C 或能够以低成本链接到 C 库的语言处于严重劣势。

本提案试图通过允许将映射到 ByteBuffer 的 NVM 进行高效写回来解决第一个问题。由于 ByteBuffer 映射的内存对 Java 来说是直接可访问的,因此可以通过实现与 C 中提供的用于管理不同持久性数据类型存储的客户端库等效的库来解决第二个问题。

描述

初步更改

此 JEP 利用了 Java SE API 的两项相关增强功能:

  1. 支持实现定义的映射模式(JDK-8221397

  2. MappedByteBuffer::force 方法以指定范围(JDK-8221696

提议的 JDK 特定 API 更改

  1. 通过新模块中的公共 API 公开新的 MapMode 枚举值

    将有一个新模块 jdk.nio.mapmode,它将导出同名的单个新包。此包中将添加一个公共扩展枚举 ExtendedMapMode

    java
    package jdk.nio.mapmode;
    . . .
    public class ExtendedMapMode {
        private ExtendedMapMode() { }
    
        public static final MapMode READ_ONLY_SYNC = . . .
        public static final MapMode READ_WRITE_SYNC = . . .
    }

    当调用 FileChannel::map 方法以分别创建映射到 NVM 设备文件的只读或读写 MappedByteBuffer 时,将使用新的枚举值。如果在不支持映射 NVM 设备文件的平台上传递这些标志,则会抛出 UnsupportedOperationException。在受支持的平台上,仅当目标 FileChannel 实例来自通过 NVM 设备打开的文件时,才应将这些新值作为参数传递。在其他任何情况下,都会抛出 IOException

  2. 发布一个跟踪持久化 MappedByteBuffer 统计信息的 BufferPoolMXBean

    ManagementFactory 类提供了方法 List<T> getPlatformMXBeans(Class<T>),可用于检索跟踪现有映射或直接字节缓冲区类别的 counttotal_capacitymemory_usedBufferPoolMXBean 实例列表。该方法将被修改以返回一个新的、额外的 BufferPoolMXBean,其名称为 "mapped - 'non-volatile memory'",它将跟踪当前以 ExtendedMapMode.READ_ONLY_SYNCExtendedMapMode.READ_WRITE_SYNC 模式映射的所有 MappedByteBuffer 实例的上述统计信息。现有的名为 mappedBufferPoolMXBean 将继续仅跟踪当前以 MapMode.READ_ONLYMapMode.READ_WRITEMapMode.PRIVATE 模式映射的 MappedByteBuffer 实例的统计信息。

提议的内部 JDK API 更改

  1. jdk.internal.misc.Unsafe 类中添加新方法 writebackMemory

    java
    public void writebackMemory(long address, long length)

    调用此方法可确保从地址 address 开始并持续至(但不一定包括)address + length 的地址范围内的任何内存修改都已从缓存写回到内存中。实现必须保证当前线程的所有存储操作(i)在调用时处于挂起状态,且(ii)在目标范围内寻址内存,都被包含在写回操作中(即,调用者无需在调用之前执行任何内存屏障操作)。它还必须保证在返回之前,所有寻址字节的写回操作已完成(即,调用者无需在调用之后执行任何内存屏障操作)。

    写回内存操作将使用 JIT 编译器识别的一小部分内部函数来实现。目标是使用一种内部函数来实现指定地址范围内每个连续缓存行的写回,该内部函数将转换为处理器缓存行写回指令,从而将数据持久化的成本降至最低。预期的设计还采用了写回前和写回后的内存同步内部函数。这些可能会转换为内存同步指令或无操作,具体取决于处理器写回指令的特定选择(x64 有三个可能的候选者)以及该选择所涉及的排序要求。

    注意

    Unsafe 类中实现此功能的一个很好的原因是,它可能具有更广泛的用途,例如用于采用非易失性内存的替代数据持久化实现。

备选方案

原始原型 中测试了两种备选方案。

一种方案是使用驱动程序模式下的 libpmem,即 1) 将 libpmem 安装为 NVM 设备的驱动程序,2) 按照任何其他 MappedByteBuffer 的方式映射文件,3) 依赖 force 方法执行更新。

第二种方案是将 libpmem(或其部分)作为 JNI 原生库来使用,以提供所需的缓冲区映射和写回行为。

两种方案均被证明非常不令人满意。第一种方案由于系统调用的高成本和强制更新整个映射缓冲区(而非其子集)的开销而遭受损失。第二种方案则由于 JNI 接口的高成本而受损。第二种方案的后续迭代(首先添加已注册的本地方法,然后将其实现为内部函数)为当前草案实现提供了类似的性能优势。

考虑的第三种备选方案是等待 Project Panama 项目提供对映射到 NVRAM 的外部库和外部数据类型的访问,而不产生 JNI 的开销。虽然这仍被视为未来的一个有价值的选择,但出于以下两个原因,我们决定继续当前提案:首先,允许用户立即从 Java 开始使用 NVRAM,因为它开始变得可用;其次,通过支持从现有、熟悉的 MappedByteBuffer API 派生的 NVRAM 使用模型,来简化这种转换所涉及的过渡。

测试

测试将需要一台配备 NVM 设备的 x64 或 AArch64 主机,并运行适当更新的 Linux 内核(4.16)。

在 AArch64 上进行测试可能不可行,直到为该架构提供合适的 NVM 设备。作为替代方案,可能需要通过映射易失性内存并使用它来模拟 NVM 设备的行为来进行测试。

在两种目标架构上进行测试可能很困难;特别是,它可能会受到误报的影响。只有当能够杀死带有未刷新待处理更改的 JVM,并在重新启动时检测到遗漏时,才能检测到回写代码中的故障。

当使用正常的 JVM 退出时(正常关闭最终可能会导致这些待处理更改被写回),这种情况可能很难安排。鉴于 JVM 无法完全控制内存系统的操作,即使执行异常退出(如 kill -KILL 终止)时,检测问题也可能变得困难。

风险与假设

此实现允许通过 ByteBuffer 将 NVM 作为堆外资源进行管理。一个相关的增强功能,JDK-8153111,正在考虑将 NVM 用于堆数据。同时,可能还需要考虑使用 NVM 来存储 JVM 元数据。这些不同的 NVM 管理模式可能会发现它们不兼容,或者当组合使用时可能不合适。

提出的 API 只能处理最多 2GB 的映射区域。可能需要对提出的实现进行修改,使其符合 JDK-8180628 中提出的更改,以克服此限制。

ByteBuffer API 主要关注基于位置的(游标)访问,这限制了对独立缓冲区区域进行并发更新的机会。这些更新需要在更新期间锁定缓冲区,如 JDK-5029431 中所述,该 JDK 也实现了一个补救措施。通过提供基本值访问器(在没有引用游标的情况下在绝对索引上操作)来允许无锁访问,以及使用 ByteBuffer 切片和 MethodHandles 来执行基本值的并发放入 / 获取,这个问题在一定程度上得到了缓解。