跳到主要内容

JEP 352:非易失性映射字节缓冲区

概括

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

目标

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

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

第二个从属目标是使用 class 中定义的受限制的 JDK 内部 API 来实现此提交行为,从而允许除可能需要提交 NVM 的Unsafe类以外的类重用它。MappedByteBuffer

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

注意:已经可以MappedByteBuffer使用当前方法将 NVM 设备文件映射到 a 并提交写入force(),例如使用 Intel 的libpmem库作为设备驱动程序或通过调用libpmem作为本机库。然而,对于当前的 API,这两种实现都提供了“大锤”解决方案。强制无法区分干净行和脏行,并且需要系统调用或 JNI 调用来实现每个写回。由于这两个原因,现有能力无法满足本 JEP 的效率要求。

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

非目标

该 JEP 的目标不仅仅限于为 NVM 提供访问和持久性保证。特别是,该 JEP 的目标不是满足其他重要行为,例如 NVM 的原子更新、读取器和写入器的隔离或独立持久内存状态的一致性。

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

成功指标

效率目标很难精确量化。然而,相对于两种现有的替代方案,将数据持久保存到内存的成本应该显着降低。首先,它应该显着改善将数据同步写入传统文件存储所产生的成本,即包括确保单个写入保证命中磁盘所需的通常延迟。其次,成本也应显着低于使用依赖于系统调用(例如libpmem.相对于同步文件写入,成本可以合理地预期降低一个数量级,相对于使用系统调用降低两倍。

动机

NVM 为应用程序员提供了跨程序运行创建和更新程序状态的机会,而不会产生通常从持久性介质输出和输入所意味着的大量复制和/或翻译成本。这对于事务性程序尤其重要,因为需要定期保留不确定状态才能实现崩溃恢复。

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

该提案试图通过允许映射到ByteBuffer.由于ByteBufferJava 可以直接访问映射内存,因此可以通过实现与 C 中提供的等效的客户端库来解决第二个问题,以管理不同持久数据类型的存储。

描述

初步变化

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

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

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

提议的 JDK 特定 API 更改

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

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

package jdk.nio.mapmode;
. . .
public class ExtendedMapMode {
private ExtendedMapMode() { }

public static final MapMode READ_ONLY_SYNC = . . .
public static final MapMode READ_WRITE_SYNC = . . .
}

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

  1. 发布BufferPoolMXBean跟踪持久MappedByteBuffer统计数据

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

提议的内部 JDK API 更改

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

    public void writebackMemory(长地址,长长度)

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

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

注意:在类中实现此功能的一个很好的理由Unsafe是它可能具有更广泛的用途,例如使用非易失性内存的替代数据持久性实现。

备择方案

在原始原型中测试了两种替代方案。

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

第二种选择是使用libpmem(或其某些片段)作为 JNI 本机库来提供所需的缓冲区映射和写回行为。

事实证明,这两种选择都非常不令人满意。第一个方法遭受系统调用的高成本以及强制使用整个映射缓冲区而不是其中的某些子集所涉及的开销。第二个是JNI接口成本高。第二种方法的连续迭代(添加第一个注册的本地函数,然后将它们实现为内在函数)提供了与当前草案实现类似的性能优势

考虑的第三种替代方案是等待巴拿马项目提供对外部库和映射到 NVRAM 的外部数据类型的访问,而不会产生 JNI 的开销。虽然这仍然被认为是未来一个值得选择的选择,但我们认为当前的提议值得推行,原因有两个:首先,当 NVRAM 开始可用时,允许用户立即尝试使用 Java 中的 NVRAM;其次,通过支持从现有的、熟悉的MappedByteBufferAPI派生的 NVRAM 使用模型来简化此类转换中涉及的转换。

测试

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

在适合该架构的 NVM 设备出现之前,可能无法在 AArch64 上进行测试。作为替代测试,可能需要通过映射易失性存储器并使用它来模拟 NVM 设备的行为来进行。

在两种目标架构上进行测试可能很困难;特别是,它可能会出现误报。仅当可以终止 JVM 并保留那些未刷新的更改,然后在重新启动时检测到该遗漏时,才能检测到写回代码中的故障。

当使用正常的 JVM 退出时,这种情况可能很难安排(正常关闭可能最终导致那些挂起的更改被写回)。由于 JVM 无法完全控制内存系统的操作,因此在kill -KILL执行异常退出(例如终止)时甚至可能很难检测到问题。

风险和假设

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

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

ByteBufferAPI 主要关注位置相关(光标)访问,这限制了对独立缓冲区进行并发更新的机会。这些需要在更新期间锁定缓冲区,如JDK-5029431中详述,该版本也实现了补救措施。通过提供原始值访问器在一定程度上缓解了这个问题,这些访问器在不引用游标的情况下以绝对索引进行操作,从而允许解锁访问;还可以选择使用ByteBuffer切片并MethodHandles执行原始值的并发放置/获取。