跳到主要内容

JEP 370:外部内存访问 API(孵化器)

概括

引入API以允许Java程序安全高效地访问Java堆之外的外部内存。

目标

外部存储器API应满足以下标准:

  • _通用性:_相同的API应该能够对各种类型的外部内存(例如,本机内存、持久内存、托管堆内存等)进行操作。
  • _安全性:_无论操作何种内存,API 都不应该破坏 JVM 的安全性。
  • _确定性:_内存释放操作应该在源代码中明确。

成功指标

外部内存 API 应该是当今 Java 程序访问外部内存的主要途径(即java.nio.ByteBuffer和)的有效替代方案sun.misc.Unsafe。新的 API 应该与这些现有的 API 具有性能竞争力。

动机

许多现有的 Java 库和程序都会访问外部内存,例如IgnitemapDBmemcached和 Netty 的ByteBuf API。通过这样做,他们可以

  • 避免与垃圾收集相关的成本和不可预测性(特别是在维护大型缓存时),
  • 跨多个进程共享内存,以及
  • 通过将文件映射到内存(例如,通过mmap)来序列化和反序列化内存内容。

然而,Java API 并没有提供令人满意的访问外部内存的解决方案。

Java 1.4 中引入的API允许创建_直接字节缓冲区,这些_ByteBuffer缓冲区在堆外分配,并允许用户直接从 Java 操作堆外内存。然而,直接缓冲区是有限的。例如,不可能创建大于 2 GB 的缓冲区,因为 API使用基于 的索引方案。此外,使用直接缓冲区可能会很麻烦,因为与直接缓冲区相关的内存的释放是留给垃圾收集器的。也就是说,只有在垃圾收集器认为直接缓冲区不可访问后,才能释放关联的内存。多年来,为了克服这些和其他限制,已经提出了许多增强请求(例如4496703、6558368、48375645029431 。其中许多限制源于这样一个事实:API不仅设计用于堆外内存访问,而且还设计用于批量数据的生产者/消费者交换,这对于字符集编码/解码和部分 I/O 操作等至关重要。ByteBuffer``int``ByteBuffer

开发人员从 Java 代码访问外部内存的另一个常见途径是 API sun.misc.Unsafe。由于巧妙且相对通用的寻址模型Unsafe,公开了许多内存访问操作(例如,Unsafe::getIntputInt),这些操作适用于堆内和堆外访问。使用Unsafe来访问内存非常高效:所有内存访问操作都被定义为 JVM 内在函数,因此内存访问操作通常由 JIT 进行优化。不幸的是,Unsafe根据定义,API 是_不安全的_——它允许访问任何内存位置(例如,Unsafe::getInt获取long地址)。这使得 Java 程序有可能在访问某些已释放的内存位置时使 JVM 崩溃。最重要的是,Unsafe API 不是受支持的 Java API,并且一直强烈建议不要使用它。

虽然使用 JNI 访问内存也是一种可能性,但与该解决方案相关的固有成本使其在实践中很少适用。整个开发流程很复杂,因为 JNI 要求开发人员编写和维护 C 代码片段。 JNI 本质上也很慢,因为每次访问都需要 Java 到本机的转换。

总而言之,当涉及到访问外部内存时,开发人员面临着一个两难境地:他们应该使用安全但有限(并且可能效率较低)的路径(例如,ByteBuffer),还是应该放弃安全保证并接受不受支持和危险的UnsafeAPI ?

此 JEP 引入了受支持的、安全且高效的外部内存访问 API。通过为访问外部内存的问题提供有针对性的解决方案,开发人员将摆脱现有 API 的限制和危险。他们还将享受到更高的性能,因为新的 API 将从头开始设计,并考虑到 JIT 优化。

描述

外部内存访问 API 引入了三个主要抽象:MemorySegmentMemoryAddressMemoryLayout

AMemorySegment用于对具有给定空间和时间边界的连续内存区域进行建模。 AMemoryAddress可以被认为是段内的偏移量。最后,aMemoryLayout是内存段内容的编程描述。

内存段可以从多种来源创建,例如本机内存缓冲区、Java 数组和字节缓冲区(直接或基于堆)。例如,可以按如下方式创建本机内存段:

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
...
}

这将创建一个与大小为 100 字节的本机内存缓冲区关联的内存段。

内存片段_在空间上是有限的;_也就是说,它们有下限和上限。任何尝试使用该段访问这些边界之外的内存都将导致异常。正如使用try-with-resource 构造所证明的那样,段也是有_时间限制的;_也就是说,它们被创建、使用,然后在不再使用时关闭。关闭段始终是显式操作,并且可能会导致额外的副作用,例如与段关联的内存的释放。任何访问已经关闭的内存段的尝试都会导致异常。总之,空间和时间安全检查对于保证内存访问 API 的安全至关重要,因此,例如,不会出现 JVM 硬崩溃。

取消引用与段关联的内存可以通过获取_内存访问变量句柄_来实现。这些特殊的 var 句柄至少有一个类型为 的强制访问坐标MemoryAddress,它是发生取消引用的地址。它们是使用MemoryHandles类中的工厂方法获得的。例如,要设置本机段的元素,我们可以使用内存访问 var 句柄,如下所示:

VarHandle intHandle = MemoryHandles.varHandle(int.class,
ByteOrder.nativeOrder());

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
MemoryAddress base = segment.baseAddress();
for (int i = 0; i < 25; i++) {
intHandle.set(base.addOffset(i * 4), i);
}
}

内存访问 var 句柄还可以获取一个或多个类型为 的额外访问坐标long,以支持更复杂的寻址方案,例如多维索引访问。这种内存访问变量句柄通常是通过调用一个或多个也在MemoryHandles类中定义的组合器方法来获得的。例如,设置本机段元素的更直接方法是通过索引内存访问句柄,其构造如下:

VarHandle intHandle = MemoryHandles.varHandle(int.class, 
ByteOrder.nativeOrder());
VarHandle intElemHandle = MemoryHandles.withStride(intHandle, 4);

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
MemoryAddress base = segment.baseAddress();
for (int i = 0; i < 25; i++) {
intElemHandle.set(base, (long) i, i);
}
}

这有效地允许对原本平坦的内存缓冲区进行丰富的多维寻址。

为了增强 API 的表达能力,并减少对诸如上述示例中的显式数值计算的需要,APIMemoryLayout可以用于以编程方式描述内存段的内容。例如,上述示例中使用的本机内存段的布局可以用以下方式描述:

SequenceLayout intArrayLayout
= MemoryLayout.ofSequence(25,
MemoryLayout.ofValueBits(32,
ByteOrder.nativeOrder()));

这将创建一个_序列_内存布局,其中给定的元素布局(32 位值)重复 25 次。一旦我们有了内存布局,我们就可以摆脱代码中的所有手动数值计算,并简化所需内存访问 var 句柄的创建,如以下示例所示:

SequenceLayout intArrayLayout
= MemoryLayout.ofSequence(25,
MemoryLayout.ofValueBits(32,
ByteOrder.nativeOrder()));

VarHandle intElemHandle
= intArrayLayout.varHandle(int.class,
PathElement.sequenceElement());

try (MemorySegment segment = MemorySegment.allocateNative(intArrayLayout)) {
MemoryAddress base = segment.baseAddress();
for (int i = 0; i < intArrayLayout.elementCount().getAsLong(); i++) {
intElemHandle.set(base, (long) i, i);
}
}

_在此示例中,布局实例通过创建布局路径_来驱动内存访问变量句柄的创建,该布局路径用于从复杂布局表达式中选择嵌套布局。布局实例还驱动本机内存段的分配,该分配基于从布局导出的大小和对齐信息。前面示例中的循环常量已替换为序列布局的元素计数。

外部内存访问 API 最初将作为孵化模块提供,名为jdk.incubator.foreign,位于同名包中。

备择方案

继续使用现有的 API,例如JNI,ByteBuffer或者Unsafe更糟糕的是 JNI。

风险和假设

创建既安全又高效的外部内存访问 API 是一项艰巨的任务。由于前面几节中描述的空间和时间检查需要在每次访问时执行,因此 JIT 能够通过例如将它们提升到热循环之外来优化这些检查至关重要。 JIT 实现可能需要做一些工作来确保内存访问 API 的使用与现有 API(例如ByteBuffer和 )的使用一样高效且可优化Unsafe

依赖关系

此 JEP 中描述的 API 可能有助于开发本机互操作支持,这是巴拿马项目的目标。该 API 还可用于以更通用和更有效的方式访问非易失性存储器,这已经可以通过JEP 352(非易失性映射字节缓冲区)实现。