JEP 370:外部内存访问 API(孵化器)
概括
引入API以允许Java程序安全高效地访问Java堆之外的外部内存。
目标
外部存储器API应满足以下标准:
- _通用性:_相同的API应该能够对各种类型的外部内存(例如,本机内存、持久内存、托管堆内存等)进行操作。
- _安全性:_无论操作何种内存,API 都不应该破坏 JVM 的安全性。
- _确定性:_内存释放操作应该在源代码中明确。
成功指标
外部内存 API 应该是当今 Java 程序访问外部内存的主要途径(即java.nio.ByteBuffer
和)的有效替代方案sun.misc.Unsafe
。新的 API 应该与这些现有的 API 具有性能竞争力。
动机
许多现有的 Java 库和程序都会访问外部内存,例如Ignite、mapDB、memcached和 Netty 的ByteBuf API。通过这样做,他们可以
- 避免与垃圾收集相关的成本和不可预测性(特别是在维护大型缓存时),
- 跨多个进程共享内存,以及
- 通过将文件映射到内存(例如,通过
mmap
)来序列化和反序列化内存内容。
然而,Java API 并没有提供令人满意的访问外部内存的解决方案。
Java 1.4 中引入的API允许创建_直接字节缓冲区,这些_ByteBuffer
缓冲区在堆外分配,并允许用户直接从 Java 操作堆外内存。然而,直接缓冲区是有限的。例如,不可能创建大于 2 GB 的缓冲区,因为 API使用基于 的索引方案。此外,使用直接缓冲区可能会很麻烦,因为与直接缓冲区相关的内存的释放是留给垃圾收集器的。也就是说,只有在垃圾收集器认为直接缓冲区不可访问后,才能释放关联的内存。多年来,为了克服这些和其他限制,已经提出了许多增强请求(例如,4496703、6558368、4837564和5029431 )。其中许多限制源于这样一个事实:API不仅设计用于堆外内存访问,而且还设计用于批量数据的生产者/消费者交换,这对于字符集编码/解码和部分 I/O 操作等至关重要。ByteBuffer``int``ByteBuffer
开发人员从 Java 代码访问外部内存的另一个常见途径是 API sun.misc.Unsafe
。由于巧妙且相对通用的寻址模型Unsafe
,公开了许多内存访问操作(例如,Unsafe::getInt
和putInt
),这些操作适用于堆内和堆外访问。使用Unsafe
来访问内存非常高效:所有内存访问操作都被定义为 JVM 内在函数,因此内存访问操作通常由 JIT 进行优化。不幸的是,Unsafe
根据定义,API 是_不安全的_——它 允许访问任何内存位置(例如,Unsafe::getInt
获取long
地址)。这使得 Java 程序有可能在访问某些已释放的内存位置时使 JVM 崩溃。最重要的是,Unsafe API 不是受支持的 Java API,并且一直强烈建议不要使用它。
虽然使用 JNI 访问内存也是一种可能性,但与该解决方案相关的固有成本使其在实践中很少适用。整个开发流程很复杂,因为 JNI 要求开发人员编写和维护 C 代码片段。 JNI 本质上也很慢,因为每次访问都需要 Java 到本机的转换。
总而言之,当涉及到访问外部内存时,开发人员面临着一个两难境地:他们应该使用安全但有限(并且可能效率较低)的路径(例如,ByteBuffer
),还是应该放弃安全保证并接受不受支持和危险的Unsafe
API ?
此 JEP 引入了受支持的、安全且高效的外部内存访问 API。通过为访问外部内存的问题提供有针对性的解决方案,开发人员将摆脱现有 API 的限制和危险。他们还将享受到更高的性能,因为新的 API 将从头开始设计,并考虑到 JIT 优化。
描述
外部内存访问 API 引入了三个主要抽象:MemorySegment
、MemoryAddress
和MemoryLayout
。
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(非易失性映射字节缓冲区)实现。