模块 java.base

包 java.lang.foreign


java.lang.foreign

提供对 Java 运行时之外的内存和函数的低级访问。

外部内存访问

为支持外部内存访问而引入的主要抽象是MemorySegment PREVIEW ,它模拟一个连续的内存区域,驻留在 Java 堆的内部或外部。内存段的内容可以使用memory layout 来描述PREVIEW ,它提供了查询大小、偏移量和对齐约束的基本操作。内存布局还为 访问内存段 使用 变量句柄 提供了另一种更抽象的方式PREVIEW ,可以使用 layout paths 计算。例如,要分配一个足够大的堆外内存区域以容纳原始类型 int 的 10 个值,并用范围从 09 的值填充它,我们可以使用以下代码:

 MemorySegment segment = MemorySegment.allocateNative(10 * 4, SegmentScope.auto());
 for (int i = 0 ; i < 10 ; i++) {
   segment.setAtIndex(ValueLayout.JAVA_INT, i, i);
 }
 
这段代码创建了一个native内存段,即由堆外内存支持的内存段;该段的大小为 40 个字节,足以存储原始类型 int 的 10 个值。该段与 自动范围 相关联PREVIEW .这意味着支持该段的堆外内存区域由垃圾收集器自动管理。因此,支持本机段的堆外内存将在某个未指定的点释放 after 该段变为 遥不可及 。这类似于通过 ByteBuffer.allocateDirect(int) 创建的直接缓冲区所发生的情况。也可以更直接地管理分配的本机段的生命周期,如后面的部分所示。

在一个循环中,我们然后初始化内存段的内容;请注意访问方法PREVIEW 接受 值布局PREVIEW ,它指定大小、对齐约束、字节顺序以及与访问操作关联的 Java 类型(在本例中为 int )。更具体地说,如果我们将内存段视为一组 10 个相邻槽,s[i],其中 0 <= i < 10,其中每个槽的大小恰好是 4 个字节,上面的初始化逻辑将设置每个槽,以便 s[i] = i,再次是 0 <= i < 10

确定性重新分配

当编写操作内存段的代码时,尤其是如果由驻留在 Java 堆之外的内存支持时,与内存段关联的资源在该段不再使用时及时释放通常是至关重要的。因此,在某些情况下,等待垃圾收集器确定段为 遥不可及 并不是最佳选择。在这些假设下运行的客户端可能希望以编程方式释放支持内存段的内存。这可以使用 Arena 来完成PREVIEW 抽象,如下图:
 try (Arena arena = Arena.openConfined()) {
   MemorySegment segment = arena.allocate(10 * 4);
   for (int i = 0 ; i < 10 ; i++) {
     segment.setAtIndex(ValueLayout.JAVA_INT, i, i);
   }
 }
 
这个例子几乎与前面的例子相同;这次我们首先创建一个arena,用于分配多个共享相同生命周期的native segment。即arena分配的所有segment都会关联同一个scopePREVIEW .请注意 try-with-resources 结构的使用:根据 14.20.3 节中描述的语义,此习惯用法可确保支持本机段的堆外内存区域将在块末尾释放Java 语言规范.

安全

该 API 在内存访问方面提供了强大的安全保证。首先,当取消引用一个内存段时,访问坐标被验证(在访问时),以确保访问不会发生在位于outside访问操作使用的内存段边界的任何地址。我们称此保证为spatial safety;换句话说,对内存段的访问是边界检查的,与数组访问的方式相同,如第 15.10.4 节所述Java 语言规范.

由于用 arena 创建的内存段可能会变得无效(见上文),段也会被验证(在访问时)以确保与正在访问的段关联的范围仍然有效。我们称此担保为temporal safety。空间和时间安全共同确保每个内存访问操作成功 - 并访问支持内存段的内存区域内的有效位置 - 或失败。

外部函数访问

为支持外部函数访问而引入的关键抽象是SymbolLookup PREVIEW , FunctionDescriptor PREVIEW Linker PREVIEW .第一个用于在库中查找符号;第二个用于建模外部函数的签名,而第三个提供链接功能,允许将外部函数建模为MethodHandle 实例,以便客户端可以直接在 Java 中执行外部函数调用,而无需 C/C++ 代码的中间层(与 Java 本机接口 (JNI) 的情况一样)。

例如,要在 Linux x64 平台上使用 C 标准库函数strlen 计算字符串的长度,我们可以使用以下代码:

 Linker linker = Linker.nativeLinker();
 SymbolLookup stdlib = linker.defaultLookup();
 MethodHandle strlen = linker.downcallHandle(
   stdlib.find("strlen").get(),
   FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
 );

 try (Arena arena = Arena.openConfined()) {
   MemorySegment cString = arena.allocateUtf8String("Hello");
   long len = (long)strlen.invoke(cString); // 5
 }
 
在这里,我们获得了一个本机链接器PREVIEW 我们用它来抬头PREVIEW 标准 C 库中的 strlen 符号; downcall method handle 以所述符号为目标随后是 获得PREVIEW .要成功完成链接,我们必须提供一个FunctionDescriptor PREVIEW 例如,描述 strlen 函数的签名。根据此信息,链接器将根据底层平台的 ABI 指定的规则,唯一地确定将方法句柄调用(此处使用 MethodHandle.invoke(java.lang.Object...) 执行)转换为外部函数调用的步骤序列。 Arena PREVIEW 类还提供了许多与外部代码交互的有用方法,例如转换PREVIEW Java 字符串转换为以零结尾的 UTF-8 字符串,如上例所示。

Upcalls

Linker PREVIEW interface 还允许客户端将现有方法句柄(可能指向 Java 方法)转换为内存段,以便 Java 代码可以有效地传递给其他外部函数。例如,我们可以编写一个比较两个整数值的方法,如下所示:
class IntComparator {
  static int intCompare(MemorySegment addr1, MemorySegment addr2) {
    return addr1.get(ValueLayout.JAVA_INT, 0) -
        addr2.get(ValueLayout.JAVA_INT, 0);

  }
}
 
上述方法访问两个包含整数值的外部内存段,并通过返回这些值之间的差异来执行简单的比较。然后我们可以获得一个针对上述静态方法的方法句柄,如下所示:
 FunctionDescriptor intCompareDescriptor = FunctionDescriptor.of(ValueLayout.JAVA_INT,
                                 ValueLayout.ADDRESS.asUnbounded(),
                                 ValueLayout.ADDRESS.asUnbounded());
 MethodHandle intCompareHandle = MethodHandles.lookup().findStatic(IntComparator.class,
                         "intCompare",
                         intCompareDescriptor.toMethodType());
 
和以前一样,我们需要创建一个FunctionDescriptor PREVIEW 例如,这次描述了我们要创建的函数指针的签名。描述符可用于derivePREVIEW 一种方法类型,可用于查找 IntComparator.intCompare 的方法句柄。

现在我们有了一个方法句柄实例,我们可以使用 Linker 将它变成一个新的函数指针PREVIEW 接口,如下:

 SegmentScope scope = ...
 MemorySegment comparFunc = Linker.nativeLinker().upcallStub(
   intCompareHandle, intCompareDescriptor, scope);
 );
 
FunctionDescriptor PREVIEW 然后将上一步中创建的实例用于createPREVIEW 一个新的 upcall 存根;函数描述符中的布局允许链接器确定允许外部代码根据底层平台 ABI 指定的规则调用 intCompareHandle 存根的步骤序列。 upcall存根的生命周期与scope相关联PREVIEW 创建上行存根时提供。 MemorySegment 提供了相同的作用域PREVIEW 该方法返回的实例。

受限方法

这个包中的一些方法被认为是 restricted 。受限方法通常用于将本地外部数据和/或函数绑定到一流的 Java API 元素,然后客户端可以直接使用这些元素。例如限制方法MemorySegment.ofAddress(long, long, SegmentScope) PREVIEW 可用于从本地地址创建具有给定空间边界的新段。

绑定外部数据和/或函数通常是不安全的,如果操作不当,可能会在访问绑定的 Java API 元素时导致 VM 崩溃或内存损坏。例如,在 MemorySegment.ofAddress(long, long, SegmentScope) 的情况下PREVIEW ,如果提供的空间边界不正确,则该方法返回的段的客户端可能会导致 VM 崩溃,或者在尝试访问所述段时损坏内存。由于这些原因,对于调用受限方法的代码来说,永远不要传递可能导致外部数据和/或函数错误绑定到 Java API 的参数是至关重要的。

考虑到受限方法的潜在危险,每次调用受限方法时,Java 运行时都会在标准错误流上发出警告。可以通过向选定模块授予对受限方法的访问权限来禁用此类警告。这可以通过特定于实现的命令行选项或以编程方式完成,例如通过调用 ModuleLayer.Controller.enableNativeAccess(java.lang.Module) PREVIEW .

对于这个包中的每个类,除非另有说明,否则任何引用类型的方法参数都不能为 null,并且任何 null 参数都将引发 NullPointerException 。这个事实没有单独记录这个 API 的方法。

API 注意:
通常的内存模型保证,例如 6.610.4 中所述,在访问本机内存段时不适用,因为这些段由堆外内存区域支持。
实现注意事项:
在参考实现中,可以使用命令行选项 --enable-native-access=M1,M2, ... Mn 向特定模块授予对受限方法的访问权限,其中 M1M2... Mn 是模块名称(对于未命名的模块,可以使用特殊值 ALL-UNNAMED)。如果指定了此选项,则仅向该选项列出的模块授予对受限方法的访问权限。如果未指定此选项,则为所有模块启用对受限方法的访问,但对受限方法的访问将导致运行时警告。