The Foreign Memory Access API is designed to replace the Unsafe and ByteBuffer APIs, addressing their limitations such as size constraints and limited access modes. It supports long offsets, enabling larger memory segments, and includes advanced access modes like volatile and compare-and-swap. It also introduces deterministic memory allocation, allowing developers to explicitly free memory when needed.
The Foreign Function API eliminates the need for an intermediate glue code layer written in C or C++. Instead, the entire intermediate layer can be written in pure Java, simplifying the process and reducing the need for additional build pipelines or learning new languages. This also improves performance due to better visibility and optimization by the JIT compiler.
The API went through several iterations, with significant redesigns of the programming model, particularly around lifetime management of memory resources. The introduction of 'arenas' allows for safer and more deterministic memory management. Additionally, support for more function shapes and platforms was added, and a fallback implementation based on libffi was introduced.
The API benefits from the 'Panama effect,' where the JIT compiler can optimize the glue code written in Java more effectively than the intermediate C/C++ code used in JNI. Additionally, upcalls (native code calling back into Java) are up to three times faster due to the efficient handling of method handles and machine code stubs.
Libraries like Lucene, Netty, and Tomcat have adopted the API, primarily to replace their usage of Unsafe. These libraries have observed performance parity with Unsafe while gaining safer memory management through the API's lifetime management features.
Developers can use the Jextract tool, which parses a native library's header file and automatically generates Java wrapper functions. This allows developers to call native library functions directly from Java without writing any C/C++ code.
The API is finalized in JDK 22 and no longer requires the --enable-preview flag. Future work includes improving the Jextract tool to generate more self-contained and editable code snippets for easier integration with Java projects.
The Foreign Function and Memory API will be finalised in JDK 22. This API helps you integrate native code within your Java program. Using this new API you can efficiently invoke code outside the JVM, safely access memory not managed by the JVM, call native libraries and process native data without the brittleness and danger of JNI. Jorn Vernee, core contributor and maintainer of the FFM API, is Ana’s guest during this episode. Jorn explains what is the Foreign Memory Access API, its goals and the iterations that this API went through. Jorn also shares a few examples of Java libraries that already adopted the FFM API, the performance improvements they observed. He also explains how you can use this new API to integrate a native library within your own Java code.</context> <raw_text>0 您好,欢迎回到 Inside Java 播客,这是一个关于 Java 的一切的播客,由 Oracle 团队(Java 的制作方)为您带来。我的名字是 Ana Maria Michelchano,我最近与 Jorn Vernet 就 JDK 22 中的外部函数和内存 API 的最终确定进行了交谈。Panama 项目和外部内存访问 API 是 Inside Java 播客第 9 集和第 10 集的主题。
但从那时起,它们经历了许多更新。因此,Jorn 和我决定让您了解导致 JDK 22 中外部函数和内存 API 最终确定的原因、这些 API 的目标、日常开发人员生活中潜在的用例以及 Panama 项目的后续内容。祝您收听愉快。Jorn,欢迎。您能否先介绍一下自己?
您好,我是 Jorn Verne。我已经从事外部函数和内存访问 API 的工作大约五年了,其中四年半是在 Oracle 工作的。在那段时间里,我主要负责外部函数 API,并且基本上重写了我继承的整个实现并将其最终确定。与此同时,
还与 Maurizio 一起讨论和设计了该 API 的公共 API 以及外部内存访问 API。哇,听起来工作量很大。让我们深入探讨今天的主题,我想先问你什么是外部内存访问 API 及其目标?
是的,外部内存访问 API 主要旨在替代不安全和字节缓冲区 API,这两个 API 都有一定的局限性。例如,字节缓冲区 API 仅基于 int 偏移量工作。这意味着字节缓冲区只能跨越有限数量的内存。
因为如果它变得太大,您将无法使用 int 偏移量访问它。因此,新的 API 使用长偏移量替换它,这实际上允许您在一个内存段中封装进程的整个虚拟地址空间。这样就可以拥有更大的内存段。最重要的是,字节缓冲区 API 在其支持的访问模式方面受到限制。
您只能进行普通访问。您可以将多个元素放入数组或从数组设置多个元素。内存访问 API 实际上是构建在 var handle 之上的,它支持许多不同的访问模式,例如设置易失性访问、比较和交换、比较和交换,这些都是非常强大的并发原语,更高级的用户可以利用它们。除此之外,
外部内存访问 API 的一个子部分是内存布局 API,这是一个可用于描述内存段或内存区域布局的 API。其思想是声明内存段的布局,然后您可以从该声明中推导出诸如某些元素、某些嵌套元素的偏移量等内容,
或推导出可用于访问内存段内某些元素的 var handle。例如,这避免了必须进行手动偏移量计算。最后,我们添加了……
此新 API 中的确定性分配,这意味着您现在可以在新 API 中显式释放内存。例如,这在字节缓冲区中是不可能的。如果您创建了一个字节缓冲区,即使它是针对本机内存的(直接字节缓冲区),
垃圾收集器实际上必须介入并为您释放该内存。因此,您无法控制何时释放内存。例如,如果您正在内存映射非常大的文件,然后在下一个循环迭代中想要内存映射下一个文件,例如,如果您正在实现数据库,则这可能会成为问题。
您需要确保在循环的下一个迭代中,当您想要映射下一个文件时,您的进程中实际上有可用的地址空间。但是,如果您必须让垃圾收集器介入并清理内存,您就无法真正依赖它。您需要能够作为开发人员说,我现在想要释放这部分内存,因为我想在继续之前内存映射下一个文件。
好的,接下来是外部函数 API,这是另一个方面。这实际上旨在替代更纯粹的 Java 的 JNI。在 JNI 中,您必须编写,为了访问您想要访问的本机库,您必须编写一个单独的中间胶水代码层。
这是一个用 C 或 C++ 编写的本机二进制文件,充当您的 Java 代码和您想要访问的本机库之间的中间体。但是对于这个新的表单函数 API,您可以在纯 Java 中执行中间层。这样做的好处是您不必学习另一种编程语言来做到这一点。您可以直接从 Java 执行任何操作。
而且,您不必设置构建管道的不同部分来构建此中间二进制文件,也不必弄清楚如何交付它。只要您的用户在其系统上安装了您尝试访问的本机库,您就可以直接使用纯 Java 代码访问它。
哇,所有这些听起来都很酷。好吧,我一直关注着这个 API 的发展,尤其是我团队维护着一个关于 Panama 的实践实验室,该实验室展示了外部函数和内存 API。因此,我对在每次孵化和预览中发生的变化有点熟悉。但是对于观众来说,您能否向我们介绍一下从第一次孵化到当前状态之间发生了什么变化以及原因?
是的,孵化器确实是 API 的实验阶段。
因此,当我们进入孵化器时,API 的某些部分尚未完成。例如,某些具有按值返回结构的本机函数调用形状未实现。因此,我们对此进行了改进。然后,尤其是在我们进行预览之后,我们进行了一些迭代,其中我们
重写并重新设计了整个编程模型,并真正地考虑了这一点。我认为在预览开始时,可以公平地说,我们拥有一套可用的功能,我认为需要三次预览迭代才能真正将其磨练成一个独立的 API,而不是仅仅是一套单独的功能。
因此,由此产生的一件事是我们研究了关于内存资源生命周期管理的编程模型。
因此,通常在 C 中,例如,您可以使用 malloc 分配内存,可以使用 free 释放内存。这意味着每个指针都有自己的生命周期。每个内存块都有自己的生命周期。并且由于 free 对每个人都是可访问的,因此任何人都可以释放该内存。
但是我们最终得到的是一个模型,在这个模型中,我们有一个称为竞技场的对象,它表示一个包含的生命周期。然后,任何访问该竞技场的人都可以在此生命周期中分配内存。最后,当竞技场关闭时,这只能由拥有竞技场并可以访问该对象的个人来完成,可以关闭并释放其中分配的所有内存。这是……
也是一个重要的安全特性,因为如果您有多个相互引用的内存块,如果它们共享相同的生命周期,则这样做是安全的。这可以防止其中一个内存块被释放,而另一个内存块仍在引用它,即使它已经消失了。所以这是我们改进的一件事。
然后对于外部函数 API,正如我所说,我们实现了一些极端情况。好吧,不是极端情况。我们实现了一些按值返回结构的函数形状。
最近,我们还添加了基于 libffi 库的回退实现。而且还有一些第三方贡献的端口。因此,我认为现在我们可以安全地说,我们也支持外部函数 API 的每个平台。在这方面,自从我们开始孵化器以来,我们已经走了很长一段路。但是大多数可见的变化都发生在内存访问 API 上。
哇,这需要很多工作,很高兴听到每个平台都受支持。多年来,包括我在内的 Java 开发人员一直使用 GNI 作为调用本机代码的唯一方法。外部函数和内存 API 有什么更好的地方?是的,所以……
当您使用 JNI 访问本机库时,您必须编写此中间胶水代码库。因此,这是一个用 C 和 C++ 编写的单独库。您必须学习与 Java 不同的编程语言才能编写这些库。您必须设置构建管道的单独部分才能构建该库。而且……
新的 API 通过使用纯 Java 模型替换编程模型来避免所有这些。您可以用纯 Java 编写您正在编写的 Java 代码和您尝试调用的本机库之间的所有互连代码。这使得……
更容易做到,因为您不必设置此构建管道。您不必一定要学习不同的编程语言,但它也有一些性能优势,例如我们所说的“巴拿马效应”,因为我们将更多此中间层引入 Java 端,
它将对 JIT 更可见,对吧?我们正在编写纯 Java 代码,它对 JIT 编译器可见。JIT 编译器可以在周围 Java 代码的上下文中一起优化该代码,而以前使用 JNI 时,您必须跳到中间库,然后跳到您尝试调用的最终库。它不是很透明。这些边界对 JIT 编译器不透明。
听起来非常好。我的意思是,从开发人员的角度来看,不必再费尽心思地在 Java 中调用本机代码是一件很棒的事情。现在,您还能告诉我们更多关于 API 的性能影响吗?比您已经与我们分享的内容更多?
是的,正如我提到的,我们有巴拿马效应,其中在 JNI 中编写的许多胶水代码现在都在 Java 端。因此,它对 JIT 编译器更透明,可以与周围的 Java 代码一起优化。但我们还花了很多时间与来自 Red Hat 的 Roland Westerlin 合作研究循环优化,用于
在主体中使用长整型或使用长整型作为索引的循环,因为新的 API 在任何需要的地方都使用长整型作为偏移量,我们需要确保它运行良好,因此我们确保这些循环的优化程度与您过去使用字节缓冲区时使用的普通 int 偏移量一样好。
但是最令人兴奋的性能优势可能在于上行调用,即本机库必须回调到 Java 的时候。我们已经设法使其比使用 JNI 快三倍。其工作方式是我们拥有外部链接器,您可以为其提供指向某些 Java 代码的方法句柄,然后外部链接器将将其包装在一个机器代码存根中
然后将其作为 C 函数指针公开。因此,C 函数指针可以传递给本机代码,然后本机代码可以使用函数指针回调到 Java。这部分大约快 3 倍,快三倍。哇,快三倍。这是一个巨大的改进。
现在,即使 Java 功能经过孵化和预览阶段,开发人员也可能希望尝试这些功能。这就是孵化和预览的目的,主要是进行实验并提供反馈。您是否知道任何特别引入、帮助或有望显著帮助外部函数和内存 API 的实验?
是的,所以有,我很高兴,我想预先说明,我很高兴我们这么早就进入了孵化阶段。
因为这确实,就像我们有很多大型项目尝试过一样。我可以脱口而出的一些大牌公司是 Lucene、Netty。我相信 Tomcat 也有。还有一个名为 Apache Data Sketches 的项目。还有一些专门为尝试 FFM API 而建立的更绿色的项目。
这提供了许多宝贵的反馈。正如我所说,Lucene 和 Netty 是其中一些较大的项目。我相信 Lucene 和 Tomcat,甚至可能是 Netty,但我并不确定,实际上已经发布了在内部使用 FFm API 的版本。
但是 Netty 和 Lucene 都使用新的 FFm API 来替换它们对 unsafe 的使用。我相信这是一个替换用例。它更多的是关于能够匹配 unsafe 的性能
而不是试图超越它,因为 Nsafe 非常底层。因此,这并不是您可以超越其性能的东西,但我们必须足够快才能与之匹敌。我认为我们做到了。然后我认为 Lucene 还发现了一些,我们设法使 Lucene 代码
我相信,更安全,因为我们拥有这个完整的生命周期管理 API,他们能够使用它来更明确地划分他们完成资源使用和释放内存的时间。这些是一些例子。哇,很高兴看到这个 API 已经帮助了项目,并且一旦它在 JDK 22 中最终确定,它可能会帮助更多项目。
除了我们目前为止讨论的内容之外,我还想知道您是否可以与我们分享任何其他用例,例如我们作为开发人员应该谨慎使用外部函数和内存 API 的地方,例如我们在使用 API 时应该注意的任何事项,或者需要注意的事项,或者实际上不应滥用 API 的地方。
是的,如果您有一个正在使用 unsafe 的项目,或者如果您有一个正在使用 JNI 的项目,或者您想要设置一个
使用本机库的项目,那么新的 FFm API 非常适合您。特别是如果您正在使用 unsafe。Unsafe 正如其名称所示,是一个不安全的 API,我们希望作为 JDK 开发人员最终将其淘汰。FFm API,尤其是内存访问部分,旨在替换 unsafe 中的一些功能。
即使名称是外部内存访问 API,该 API 也可用于访问 Java 堆内存。因此,您可以将 Java 数组包装在内存段中,或者您可以将 Java 和 NIO 缓冲区包装在内存段中,然后也可以使用新的 API 访问该内存。很高兴听到这个消息。
假设某人有一个本机库并希望将其集成到一些 Java 代码中,他们将如何开始这样做?您如何开始使用外部函数内存 API(可能还有一些工具)在 Java 代码中集成本机库?
是的,是的。如果您有一个本机库,那么最好的起点是有一些要求。本机库需要具有 C 接口。然后您将能够使用 FM API。最好的起点是使用 J extract 工具,这是一个独立工具。因此,它不包含在 JDK 中。这是我们开发的并且仍在开发的独立工具。
可用于解析本机库的头文件并自动生成可用于调用该本机库的 Java 包装器函数。因此,您真正需要的只是下载 Jxtract。它位于 GitHub 上,地址为 github.com/openjk/jxtract。README 中有关于如何获取它、如何下载它的说明。
您真正需要的只是本机库头文件、单个命令行调用,您就可以获得访问库所需的源代码或类文件。因此,其思想是您运行 J extract,然后将它生成的类或源文件包含在您的项目中,然后您可以从那里调用它。它与 C 几乎完全相同。它生成一个头文件
一个类,本质上,如果我有一些像 foo 这样的库,那么 jstriq 将生成一个 foo_h 类文件,您可以使用 import static foo_h.star 来使用它,它将导入所有函数,您可以像 C 一样调用它们。
听起来很有趣。我一直在使用 Jxtract,有时自己构建它。您也可以这样做,或者只是下载它并使用它。这取决于您希望多快地使用 JDK 早期访问版本中的功能进行开发。
Jorn,非常感谢您提供的所有这些有益信息。关于 Panama 项目,您还有什么想补充的吗?目前或将来是什么让您对它感到兴奋,如果您有任何想与我们分享的内容或让我们关注和跟踪的内容?
是的,就 Panama 项目路线图而言,就函数和内存访问 API 而言,我们已在 JDK 22 中最终确定了 API。因此,它已从预览状态移至最终完全支持的功能。我不应该说完全支持,因为预览功能也完全支持。
但是,是的,外部函数内存 API 在 jdk 22 中最终确定了
您将能够在没有 --enable-preview 标志的情况下使用它。API 的某些部分具有受限功能,这些功能本质上是在使用不正确时可能会使 VM 崩溃的功能。这些功能受 --enable-native-access 标志保护,但这主要……
这主要针对表单函数 API 的某些部分。表单内存访问 API 几乎是纯 Java。如果您不关心对本机库的访问,您可以直接使用它。然后,是的,所以在 JDK 22 中最终确定,然后我们仍在努力改进 JXtract。我们实际上正在重新考虑它生成的类的模型。
我们希望使其能够生成更多自包含的代码片段,如果您愿意,可以更容易地编辑它们,或者如果您愿意,可以将它们复制粘贴到其他地方。是的,这就是我们目前正在做的工作。
谢谢,Jorn。感谢您今天加入我们并与我们分享所有这些见解。各位,我们现在要结束了。如果您想了解更多关于 Panama 的信息,请查看 insight.java.com 上汇总的联系方式。您可以找到从事 Panama 项目、Jet Café、SIP、新闻报道以及更多内容的经验丰富的工程师的演讲和博客,所有这些都旨在帮助您成功利用最新的 Java 开发成果。
再次感谢 Jorn 今天加入我。我们的听众们,请继续关注 Java YouTube 频道,以了解更多关于 Java 的好消息。JORN LINDEN:感谢您的邀请。