随着 JDK 14 的发布,新版带来了很多全新或预览的功能,如 instanceof 模式匹配、信息量更多的 NullPointerExceptions、switch 表达式等。大部分功能已经被许多新闻和博客网站广泛报道,但是孵化中的外部内存访问 API 还没有得到那么多的报道,许多报道 JDK 14 的新闻都省略了它,或者只提到了 1-2 行。很可能没有多少人知道它,也不知道它最终会允许你在 Java 中做什么。
简而言之,外部内存访问 API 是 Project Panama (1) 的一部分,是对 ByteBuffer 的替代,而 ByteBuffer 之前是用于堆外内存。对于任何低级的 I/O 来说,堆外内存是需要的,因为它避免了 GC,从而比堆内内存访问更快、更可靠。但是,ByteBuffer 也存在局限,比如 2GB 的大小限制等。
如果你想了解更多,你可以在下面链接观看 Maurizio Cimadamore 的演讲 (2)。
正如上面的视频所描述的那样,孵化外部内存访问 API 并不是最终的目标,而是通往更高的目标:Java 中的原生 C 库访问。遗憾的是,目前还没有关于何时交付的时间表。
话虽如此,如果你想尝试真正的好东西,那么你可以从 Github (3) 中构建自己的 JDK。我一直在做这个工作,为我的超频工具所需要的各种 Nvidia API 做绑定,这些 API 利用 Panama 的抽象层来使事情变得更简单。
说了这么多,那你实际是怎么使用它的呢?
MemoryAddress 以及 MemorySegment
Project Panama 中的两个主要接口是 MemoryAddress 和 MemorySegment。在外部内存访问 API 中,获取 MemoryAddress 首先需要使用静态的 allocateNative() 方法创建一个 MemorySegment,然后获取该段的基本地址。
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
public class PanamaMain
{
public static void main(String[] args)
{
MemoryAddress address = MemorySegment.allocateNative(4).baseAddress();
}
}
当然,你可以通过 MemoryAddress 的 segment() 方法再次获取同一个 MemoryAddress 的段。在上面的例子中,我们使用的是重载的 allocateNative() 方法,该方法接收了一个新的 MemorySegment 的字节大小的 long 值。这个方法还有另外两个版本,一个是接受一个 MemoryLayout,我稍后会讲到,另一个是接受一个以字节为单位的大小和字节对齐。
MemoryAddress 本身并没有太多的API。唯一值得注意的方法是 segment() 和 offset() 。没有获取 MemoryAddress 的原始地址的方法。
而 MemorySegment 则有更多的 API。你可以通过 asByteBuffer() 将 MemorySegment 转换为 ByteBuffer,通过 close() 关闭(读:free)段(来自 AutoClosable 接口),然后用 asSlice() 将其切片(后面会有更多的内容)。
好了,我们已经分配了一大块内存,但如何对它进行读写呢?
MemoryHandle
MemoryHandles 是一个提供 VarHandles 的类,用于读写内存值。它提供了一些静态的方法来获取 VarHandle,但主要的方法是 varHandle,它接受下面任一类。
byte.class
short.class
char.class
int.class
double.class
long.class
(这些都不能和Object版本混淆,比如Integer.class)
在大多数情况下