JEP 412:外部函数和内存API(孵化器)

所有者毛里齐奥·西马达摩尔
类型功能
范围JDK公司
状态关闭/交付
发布17
组件核样物质
讨论openjdk dot-java dot-net上的巴拿马破折号开发
努力M(M)
持续时间M(M)
与相关JEP 424:外部函数和内存API(预览版)
JEP 389:外国链接器API(孵化器)
JEP 393:外部内存访问API(第三孵化器)
JEP 419:外部函数和内存API(第二孵化器)
审核人Adam Pocock、Alex Buckley、Paul Sandoz
创建2021/04/10 21:05
已更新2022/03/02 17:11
问题8265033

总结

引入一个API,通过它,Java程序可以与Java运行时之外的代码和数据进行互操作。通过有效地调用外部函数(即JVM外部的代码),并通过安全地访问外部内存(即不由JVM管理的内存),API使Java程序能够调用本机库并处理本机数据,而不会出现JNI的脆弱性和危险性。

历史

本JEP中提出的API是两个正在酝酿中的API的演变:Foreign-Memory Access API和Foreign Linker API。Foreign-Memory Access API最初由提出JEP 370型并于2019年末以Java 14为目标孵化API; 它被重新吸收JEP 383型Java 15和JEP 393号机组Java 16。Foreign Linker API最初由提出第389页并于2020年末针对Java 16孵化API.

目标

非目标

这不是一个目标

动机

Java平台始终为希望超越JVM并与其他平台交互的库和应用程序开发人员提供了丰富的基础。Java API以方便可靠的方式公开非Java资源,无论是访问远程数据(JDBC)、调用web服务(HTTP客户端)、服务远程客户端(NIO通道)还是与本地进程(Unix域套接字)通信。不幸的是,Java开发人员在访问一种重要的非Java资源(即与JVM在同一台机器上但在Java运行时之外的代码和数据)时仍然面临着巨大的障碍。

外部存储器

存储在Java运行时外部内存中的数据称为非堆存储数据。Java对象所在的位置-堆上访问堆外数据对于流行的Java库(例如Tensorflow公司,点燃,Lucene公司、和Netty公司这主要是因为它可以避免与垃圾收集相关的成本和不可预测性。它还允许通过将文件映射到内存来序列化和反序列化数据结构,例如。,甲基丙烯酸甲酯然而,Java平台目前还不能为访问堆外数据提供令人满意的解决方案。

总之,在访问堆外数据时,Java开发人员面临着一个两难境地:他们是否应该选择一条安全但效率低下的路径(字节缓冲区)或者他们应该放弃安全而追求性能(不安全的)? 他们所需要的是一个受支持的API,用于访问堆外数据(即外部内存),该API从头开始就设计为安全的,并考虑到JIT优化。

国外职能

自Java1.1以来,JNI一直支持调用本机代码(即外部函数),但由于许多原因,它是不够的。

多年来,出现了许多框架来填补JNI留下的空白,包括JNA公司,JNR公司JavaCPP(JavaCPP)虽然这些框架通常是对JNI的显著改进,但情况仍然不太理想,尤其是与提供一流本地互操作的语言相比。例如,Python的C型包可以在本地库中动态包装函数,而无需任何粘合代码。其他语言,例如生锈,提供从C/C++头文件机械派生本机包装器的工具。

最终,Java开发人员应该有一个受支持的API,让他们能够直接使用任何被认为对特定任务有用的本机库,而不必像JNI那样冗长乏味。一个很好的构建抽象是方法句柄在Java 7中引入,以支持JVM上的快速动态语言。通过方法句柄公开本机代码将从根本上简化编写、构建和分发依赖本机库的Java库的任务。此外,能够建模外部函数(即本机代码)和外部内存(即堆外数据)的API将为第三方本机互操作框架提供坚实的基础。

描述

外部函数和内存API(FFMAPI)定义类和接口,以便库和应用程序中的客户端代码可以

FFM API驻留在jdk.孵化器.国外的包jdk.孵化器.国外模块。

例子

作为使用FFMAPI的一个简短示例,下面是获得C库函数的方法句柄的Java代码基数排序然后使用它对Java数组中开始生命的四个字符串进行排序(省略了一些细节):

// 1. 在C库路径上查找外部函数MethodHandle radixSort=CLinker.getInstance().downcall句柄(CLinker.systemLookup().lookup(“基数排序”),…);// 2. 分配堆内存以存储四个字符串String[]javaStrings={“鼠标”,“猫”,“狗”,“汽车”};// 3. 分配堆外内存以存储四个指针MemorySegment offHeap=内存段.allocateNative(序列的内存布局(javaStrings.length,C链接器。C_POINTER),…);// 4. 将字符串从堆上复制到堆外for(int i=0;i<javaStrings.length;i++){//从堆外分配字符串,然后存储指向它的指针MemorySegment cString=CLinker.toCString(javaStrings[i],newImplicitScope());MemoryAccess.setAddressAtIndex(offHeap,i,cString.address());}// 5. 通过调用外部函数对堆外数据进行排序radidSort.invoke(offHeap.address(),javaStrings.length,MemoryAddress)。空,“\0”);// 6. 将(重新排序的)字符串从堆外复制到堆内for(int i=0;i<javaStrings.length;i++){MemoryAddress cStringPtr=MemoryAccess.getAddressAtIndex(offHeap,i);javaStrings[i]=CLinker.toJavaStringRestricted(cStringPtr);}assert Arrays.equals(javaStrings,new String[]{“car”,“cat”,”dog“,”mouse“});//真的

这段代码比任何使用JNI的解决方案都要清晰得多,因为隐式转换和内存取消引用是隐藏在后面的本地的方法调用现在直接用Java表示。也可以使用现代Java习语;例如,流可以允许多个线程并行地在堆内和堆外内存之间复制数据。

内存段

A类存储器段是对位于堆外或堆上的连续内存区域进行建模的抽象。内存段可以是

所有内存段都提供了空间、时间和线程约束保证,这些保证都得到了严格执行,以确保内存去引用操作的安全。例如,以下代码在堆外分配100字节:

MemorySegment段=MemorySegrament.allocateNative(100,newImplicitScope());

这个空间界限确定与段关联的内存地址的范围。上述代码中段的边界由基址 b条,表示为内存地址实例,以及字节大小(100),导致从b条b条+99(含)。

这个时间界限段的长度决定了段的生存期,即段将被释放的时间。段的生存期和线程限制状态由资源范围抽象,已讨论在下面。上述代码中的资源范围是一个新的隐性的作用域,确保当内存段垃圾收集器认为对象不可访问。隐式作用域还确保可以从多个线程访问内存段。

换句话说,上面的代码创建了一个段,其行为与字节缓冲区分配给分配直接工厂。FFM API还支持确定性内存释放和其他线程限制选项,如前所述在下面.

取消引用内存段

通过获取var句柄是Java9中引入的数据访问抽象。特别是,段被取消引用内存访问变量句柄。这种var句柄使用一对访问坐标:

内存访问var句柄是通过内存句柄类。例如,此代码获取一个memory-accessvar句柄,该句柄可以编写整数值写入本机内存段,并使用它以连续偏移量写入25个四字节值:

MemorySegment段=MemorySegrament.allocateNative(100,newImplicitScope());VarHandle intHandle=内存句柄.VarHandle(int.class,ByteOrder.nativeOrder());对于(int i=0;i<25;i++){intHandle.set(段,/*偏移量*/i*4,/*写入值*/i);}

通过使用由内存句柄类。通过这些,客户端可以重新排序给定内存访问变量句柄的坐标,删除一个或多个坐标,并插入新坐标。这允许创建内存访问变量句柄,该句柄将一个或多个逻辑索引接受到由平坦的堆外内存区域支持的多维数组中。

为了使FFM API更容易接近内存访问类提供静态访问器来取消引用内存段,而无需构造memory-access-var句柄。例如,有一个访问器来设置整数给定偏移量段中的值,允许将上面的代码简化为:

MemorySegment段=MemorySegrament.allocateNative(100,newImplicitScope());对于(int i=0;i<25;i++){MemoryAccess.setIntAtOffset(段,i*4,i);}

内存布局

为了减少对内存布局进行繁琐计算的需要(例如。,i*4个在上述示例中)内存布局可用于以更具声明性的方式描述内存段的内容。例如,可以以下列方式描述上述示例中本机内存段的所需布局:

数组布局中的序列布局=MemoryLayout.sequenceLayout(25,MemoryLayout.valueLayout(32,ByteOrder.nativeOrder());

这将创建一个序列存储器布局其中32位价值布局(描述单个32位值的布局)重复25次。给定内存布局,我们可以避免计算代码中的偏移量,并简化内存分配和内存访问变量句柄的创建:

MemorySegment段=MemorySegram.allocateNative(intArrayLayout,newImplicitScope());VarHandle索引元素句柄=intArrayLayout.varHandle(int.class,PathElement.sequenceElement());for(int i=0;i<intArrayLayout.elementCount().getAsLong();i++){indexedElementHandle.set(段,(长)i,i);}

这个intArray布局对象通过创建布局路径,用于从复杂布局表达式中选择嵌套布局。这个intArray布局对象还驱动本机内存段的分配,这基于从布局导出的大小和对齐信息。前面示例中的循环常数,25已替换为序列布局的元素计数。

资源作用域

在前面的示例中看到的所有内存段都使用了非确定性释放:一旦内存段实例变得无法访问,与这些段关联的内存将由垃圾收集器释放。我们说这样的部分是隐式释放.

在某些情况下,客户端可能希望控制内存释放的发生时间。例如,假设使用内存段::映射。客户端可能更喜欢在不再需要该段时释放(即取消映射)与该段关联的内存,而不是等待垃圾回收器这样做,因为等待可能会对应用程序的性能产生不利影响。

内存段支持确定性释放资源作用域。资源范围对与一个或多个相关联的生命周期进行建模资源,例如内存段。新创建的资源范围位于活着的状态,这意味着它管理的所有资源都可以安全访问。根据客户端的请求,资源范围可以是关闭,这意味着不再允许访问范围管理的资源。这个资源范围类实现自动关闭接口,以便资源范围与尝试使用资源声明:

try(ResourceScope scope=ResourceScope.newConfigedScope()){MemorySegments1=MemorySegrament.map((“someFile”)的路径,0,100000,MapMode(地图模式)。READ_WRITE,范围);MemorySegment s2=MemorySegrament.allocateNative(100,范围);...}//此处发布了两个段

此代码创建受限制的资源范围并将其用于创建两个段:映射段(第1页)和本机段(s2秒). 这两个段的生命周期与资源范围的生命周期有关,因此在try-with-resources语句完成后访问这些段(例如,使用memory-access-var句柄取消对它们的引用)将导致抛出运行时异常。

除了管理内存段的生存期外,资源作用域还可以控制哪些线程可以访问该段。受限资源范围限制对创建范围的线程的访问,而共享资源范围允许从任何线程进行访问。

资源范围,无论是受限的还是共享的,都可以与java.lang.ref.清理器对象,该对象负责在资源范围对象在关闭方法由客户端调用。

一些资源范围,称为隐性的资源作用域,不支持显式释放-调用关闭将失败。隐式资源范围始终使用保洁员。可以使用资源范围::newImplicitScope工厂,如前面的示例所示。

段分配器

当客户端使用堆外内存时,内存分配通常会成为瓶颈。FFM API包括分段分配器抽象,它定义了分配和初始化内存段的有用操作。段分配器是通过分段分配器接口。例如,下面的代码创建了一个基于arena的分配器,并使用它分配一个内容从Java初始化的段整数数组:

try(ResourceScope scope=ResourceScope.newConfigedScope()){SegmentAllocator分配器=SegmentAllocator.arenaAllocator(scope);对于(int i=0;i<100;i++){MemorySegments=分配器.allocateArray(C_INT,new INT[]{1,2,3,4,5});...}...}//此处释放分配的所有内存

此代码创建一个受限的资源范围,然后创建一个无界竞技场分配器与该范围关联。这个分配器将分配特定大小的内存块,并通过返回预先分配的内存块的不同片来响应分配请求。如果板没有足够的空间来容纳新的分配请求,则会分配新的板。如果与竞技场分配器相关联的资源作用域被关闭,则与分配器创建的段相关联的所有内存(即,在对于循环)以原子方式释放。此习惯用法结合了确定性释放的优点,由资源范围抽象,具有更灵活和可扩展的分配方案。它在编写管理大量堆外段的代码时非常有用。

不安全的内存段

到目前为止,我们已经看到了内存段、内存地址和内存布局。取消引用操作只能在内存段上进行。由于内存段具有空间和时间边界,Java运行时始终可以确保与给定段关联的内存被安全地解除引用。然而,在某些情况下,客户端可能只有内存地址实例,这是与本机代码交互时经常出现的情况。由于Java运行时无法知道与内存地址关联的空间和时间边界,因此FFM API禁止直接取消引用内存地址。

要取消引用内存地址,客户端有两个选项。

查找外部函数

对外部函数的任何支持的第一个要素是加载本机库的机制。使用JNI,可以使用系统::loadLibrary系统::加载方法,它在内部映射为对的调用dlopen(dlopen)或其等效物。使用这些方法加载的库总是与类加载器(即调用系统方法)。库和类装入器之间的关联至关重要,因为它控制生命周期加载的库的:只有当类加载器不再可访问时,才能卸载其所有库安全地.

FFM API没有提供加载本机库的新方法。开发人员使用系统::loadLibrary系统::加载方法加载将通过FFM API调用的本机库。库和类加载器之间的关联得到了保留,因此库将以与JNI相同的可预测方式卸载。

与JNI不同,FFMAPI提供了在加载的库中查找给定符号地址的功能。此功能由符号查找对象,对于将Java代码链接到外部函数至关重要(请参见在下面). 有两种方法可以获得符号查找对象:

给定符号查找,客户端可以使用符号查找::查找(字符串)方法。如果命名函数出现在符号查找所看到的符号中,则该方法返回一个内存地址它指向函数的入口点。例如,以下代码加载OpenGL库(使其与当前类加载器相关联)并查找其地址glGetString(全局获取字符串)功能:

System.loadLibrary(“GL”);SymbolLookup loaderLookup=SymbolLookup.loaderLookup();MemoryAddress clangVersion=loaderLookup.lookup(“glGetString”).get();

将Java代码链接到外部函数

这个C链接器接口是Java代码与本机代码互操作的核心。C链接器专注于提供Java和C库之间的互操作,接口中的概念非常通用,足以支持未来的其他非Java语言。该接口支持叫停(从Java代码到本机代码的调用)和向上调用(从本机代码调用回Java代码)。

接口CLinker{MethodHandle downcallHandle(MemoryAddress函数,MethodType类型,FunctionDescriptor函数);MemoryAddress upcallStub(MethodHandle目标,FunctionDescriptor函数,ResourceScope范围);}

对于叫停向下调用句柄方法获取外部函数的地址-通常是内存地址从库查找中获取-并将外部函数公开为downcall方法句柄稍后,Java代码通过调用其调用精确方法,并运行外部函数。传递给方法句柄的任何参数invokeExact(调用精确)方法传递给外部函数。

对于upcallsupcall存根该方法接受一个方法句柄(通常指的是Java方法,而不是downcall方法句柄),并将其转换为内存地址。稍后,当Java代码调用downcall方法句柄时,内存地址作为参数传递。实际上,内存地址充当函数指针。(有关向上调用的更多信息,请参阅在下面.)

假设我们希望从Java向下调用斯特伦标准C库中定义的函数:

size_t字符串(常量字符*s);

公开的downcall方法句柄斯特伦可以通过以下方式获得(详细信息方法类型功能描述符稍后将进行介绍):

MethodHandle strlen=CLinker.getInstance().downcall句柄(CLinker.systemLookup().lookup(“strlen”).get(),MethodType.MethodType(long.class,MemoryAddress.class),(C_LONG,C_POINTER)的FunctionDescriptor);

调用downcall方法句柄将运行斯特伦并使其结果在Java中可用。对于参数斯特伦,我们使用helper方法将Java字符串转换为非堆内存段,并传递该段的地址:

MemorySegment str=CLinker.toCString(“Hello”,newImplicitScope());long len=strlen.invokeExact(str.address());//5

方法句柄可以很好地用于公开外部函数,因为JVM已经优化了方法句柄的调用,一直到本机代码。当方法句柄引用文件,调用方法句柄通常会导致目标方法被JIT编译;随后,JVM解释调用的Java字节码MethodHandle::invokeExact通过将控制权转移到为目标方法生成的汇编代码。因此,调用传统方法句柄已经是准外部调用;以C库中的函数为目标的向下调用方法句柄只是一种更加国际化的方法句柄。方法句柄还具有一个名为特征多态性它允许使用原始参数进行无框调用。总之,方法句柄使C链接器以自然、高效和可扩展的方式公开外部函数。

用Java描述C类型

为了创建向下调用方法句柄,FFM API要求客户端提供目标C函数的双面视图:使用不透明的Java对象(内存地址,内存段)和使用的低级签名透明的Java对象(内存布局). 依次获取每个签名:

例如,获得C函数的downchall方法句柄整数并返回一个长的将需要以下内容方法类型功能描述符参数到向下调用句柄:

MethodType mtype=MethodType.MethodType(long.class,int.class);FunctionDescriptor fdesc=(C_LONG,C_INT)的函数描述符;

(此示例针对Linux/x64和macOS/x64,其中Java类型长的整数与预定义的C链接器布局长(_LONG)C_INT(_IN)分别是。Java类型与内存布局的关联因平台而异;例如,在Windows/x64上,Java长的与关联C_LONG_LONG(长_长)布局。)

作为另一个示例,为空隙接受指针的C函数需要以下内容方法类型功能描述符:

MethodType mtype=MethodType.MethodType(void.class,MemoryAddress.class);FunctionDescriptor fdesc=无效函数描述符(C_POINTER);

(C中的所有指针类型表示为内存地址Java中的对象;相应的布局(其大小取决于当前平台)为指针(_POINTER)客户不区分,例如。,整数*字符**,因为传递给C链接器共同包含足够的信息,以便将Java参数正确传递给C函数。)

最后,与JNI不同的是C链接器支持将结构化数据传递给外部函数。获取的downcall方法句柄空隙接受结构的C函数需要以下内容方法类型功能描述符:

MethodType mtype=MethodType.MethodType(void.class,MemorySegment.class);MemoryLayout SYSTEMTIME=结构的内存布局(C_SHORT.withName(“wYear”),C_SHORT withName(”wMonth“),C_SHORT.withName(“wDayOfWeek”),C_SHORT withName(”wDay“),C_SHORT.withName(“wHour”),C_SHORT withName((“wMinute”)),C_SHORT.withName(“wSecond”),C_SHORT withName((“wMilliseconds”));FunctionDescriptor fdesc=无效的功能描述符(SYSTEMTIME);

(对于高层方法类型签名,Java客户端始终使用不透明类型内存段其中C函数需要通过值传递的结构。对于低层功能描述符签名,与C结构类型关联的内存布局必须是一个复合布局,该布局定义了C结构中所有字段的子布局,包括可能由本机编译器插入的填充。)

如果C函数返回由低级签名表示的by-value结构,那么必须在堆外分配一个新内存段并将其返回给Java客户端。为了实现这一点向下调用句柄需要额外的分段分配器参数,FFM API使用该参数分配内存段以保存C函数返回的结构。

打包C函数的Java参数

不同语言之间的交互需要呼叫约定指定一种语言中的代码如何调用另一种语言的函数,如何传递参数,以及如何接收任何结果。这个C链接器该实现了解几种现成的调用约定:Linux/x64、Linux/AArch64、macOS/x64和Windows/x64。它是用Java编写的,比JNI更容易维护和扩展,JNI的调用约定被硬连接到HotSpot的C++代码中。

考虑上面显示的SYSTEMTIME(系统时间)结构和布局。给定JVM运行的操作系统和CPU的调用约定C链接器使用函数描述符推断当使用内存段参数。对于一个呼叫约定C链接器可以安排分解传入内存段,使用通用CPU寄存器传递前四个字段,并传递C堆栈上的其余字段。对于不同的调用约定C链接器可以安排FFM API通过分配内存区域间接传递结构,将传入内存段的内容批量复制到该区域,并将指向该内存区域的指针传递给C函数。这种最低级别的参数打包是在幕后进行的,不需要客户机代码的监督。

向上呼叫

有时将Java代码作为函数指针传递给某个外部函数很有用。我们可以通过使用C链接器支持upcalls。在本节中,我们逐一构建了一个更复杂的示例,该示例演示了C链接器,跨Java/本机边界实现代码和数据的完全双向互操作。

考虑标准C库中定义的以下函数:

void qsort(void*base,size_t nmemb,size-t size,int(*compar)(const void*,const void*));

拨打电话快速排序在Java中,我们首先需要创建一个downcall方法句柄:

MethodHandle qsort=CLinker.getInstance().downcallHandle(CLinker.systemLookup().lookup(“qsort”).get(),MethodType.MethodType(void.class、MemoryAddress.class和long.class,long.class,MemoryAddress.class),Void的函数描述符(C_POINTER、C_LONG、C_LON、C_POINTER));

和以前一样,我们使用长(_LONG)长类映射C尺寸_t类型,我们使用内存地址.class第一个指针参数(数组指针)和最后一个参数(函数指针)。

快速排序使用自定义比较器功能对数组的内容进行排序,比较,作为函数指针传递。因此,要调用下调用方法句柄,我们需要一个函数指针作为方法句柄的最后一个参数传递调用精确方法。CLinker::upcallStub帮助我们使用现有的方法句柄创建函数指针,如下所示。

首先,我们写一个静止的Java中比较两个长的值,间接表示为内存地址物体:

类Qsort{静态int qsortCompare(内存地址addr1,内存地址addr 2){return MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(),将1.toRawLongValue())-MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(),addr2.toRawLongValue());}}

其次,我们创建了一个指向Java comparator方法的方法句柄:

MethodHandle比较句柄=MethodHandles.lookup().findStatic(Qsort.class,“qsortCompare”,MethodType.MethodType(int.class,内存地址类,MemoryAddress.class));

第三,现在我们有了Java比较器的方法句柄,可以使用CLinker::upcallStub。与向下调用一样,我们使用中的布局描述函数指针的签名C链接器类别:

内存地址比较功能=CLinker.getInstance().upcallStub(comparHandle,(C_INT,指针(_POINTER),C_POINTER),newImplicitScope()););

我们终于有了一个内存地址,比较函数,它指向一个可用于调用Java比较器函数的存根,因此现在我们已经具备了调用快速排序下拉手柄:

内存段数组=MemorySegment.allocateNative(4*10,newImplicitScope());array.copyFrom(数组的MemorySegment.ofArray(新的int[]{0,9,3,4,6,5,1,8,2,7}));qsort.invokeExact(array.address(),10L,4L,comparFunc);int[]排序=数组.toIntArray();//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

此代码创建一个非堆数组,将Java数组的内容复制到其中,然后将数组传递给快速排序处理从C链接器。调用后,将根据用Java编写的比较器函数对堆外数组的内容进行排序。然后,我们从包含排序元素的段中提取一个新的Java数组。

安全

从根本上讲,Java代码和本机代码之间的任何交互都可能损害Java平台的完整性。链接到预编译库中的C函数本质上是不可靠的,因为Java运行时无法保证函数的签名与Java代码的预期相符,甚至无法保证C库中的符号实际上是一个函数。此外,如果链接了合适的函数,则实际调用该函数可能会导致低级失败,例如分段错误,最终导致VM崩溃。Java运行时无法防止此类故障,Java代码也无法捕获此类故障。

使用JNI函数的本机代码尤其危险。这样的代码可以在不使用命令行标志(例如。,--附加开关),通过使用以下函数获取静态字段调用虚拟方法。它还可以更改最终的字段初始化后很长时间。允许本机代码绕过应用于Java代码的检查会破坏JDK中的每个边界和假设。换句话说,JNI本质上是不安全的。

无法禁用JNI,因此无法确保Java代码不会调用使用危险JNI函数的本机代码。这是对平台完整性的风险,应用程序开发人员和最终用户几乎看不到这一风险,因为这些功能的99%使用通常来自夹在应用程序和JDK之间的第三方、第四方和第五方库。

大多数FFM API在设计上是安全的。过去许多需要使用JNI和本机代码的场景都可以通过调用FFM API中的方法来完成,这不会影响Java平台。例如,JNI的一个主要用例,即灵活的内存分配,通过一个简单的方法得到支持,内存段::allocateNative,它不涉及本机代码,始终返回由Java运行时管理的内存。一般来说,使用FFM API的Java代码不会使JVM崩溃。

然而,FFM API的一部分本质上是不安全的。与交互时C链接器,Java代码可以通过指定与底层C函数不兼容的参数类型来请求downcall方法句柄。在Java中调用downchall方法句柄将导致与调用本地的JNI中的方法。FFM API还可能产生不安全的段,即其空间和时间边界由用户提供,无法由Java运行时验证的内存段(请参阅内存地址::as段).

FFM API中的不安全方法不会带来与JNI功能相同的风险;例如,他们不能更改最终的Java对象中的字段。另一方面,FFMAPI中的不安全方法很容易从Java代码调用。因此,在FFM API中使用不安全的方法是受限制的:默认情况下禁用对不安全方法的访问,因此调用此类方法会引发非法访问例外。要为某些模块M中的代码启用对不安全方法的访问,请指定java--enable-native-access=M在命令行上。(在逗号分隔的列表中指定多个模块;指定全部未修订以启用类路径上所有代码的访问。)FFMAPI的大多数方法都是安全的,Java代码可以使用这些方法,无论--启用主动访问给出了。

我们在此不建议限制JNI的任何方面。仍然可以拨打本地的Java中的方法,以及本地代码调用不安全的JNI函数。然而,我们可能会在未来的版本中以某种方式限制JNI。例如,不安全的JNI函数,例如新DirectByte缓冲区默认情况下可以禁用,就像FFM API中的不安全方法一样。更广泛地说,JNI机制是如此危险,以至于我们希望库将更喜欢pure-Java FFM API来进行安全和不安全的操作,以便我们能够及时禁用所有JNI。这与更广泛的Java路线图一致,即让平台安全、现成,要求最终用户选择不安全的活动,例如破坏强封装或链接到未知代码。

我们不建议在这里改变sun.misc公司。不安全的无论如何。FFMAPI对堆外内存的支持是对包装器的一个很好的替代malloc公司自由的在里面sun.misc公司。不安全的,即分配内存,设置内存,复制内存、和空闲内存。我们希望需要堆外存储的库和应用程序采用FFM API,以便我们能够及时弃用并最终删除这些API周日。不安全的方法。

选择

继续使用java.nio。字节缓冲区,周日。不安全的、JNI和其他第三方框架。

风险和假设

创建一个API以安全高效的方式访问外部内存是一项艰巨的任务。由于前面章节中描述的空间和时间检查需要在每次访问时执行,因此JIT编译器能够通过将这些检查提升到热循环之外来优化这些检查是至关重要的。JIT实现可能需要一些工作来确保API的使用与现有API的使用一样高效和可优化,例如字节缓冲区不安全的。JIT实现还需要进行工作,以确保从API检索的本机方法句柄的使用至少与现有JNI本机方法的使用一样高效和可优化。

依赖关系