jdk21的外部函数和内存API(MemorySegment)(官方翻译)
1、jdk21: 引入一个 API,通过该 API,Java 程序可以与 Java 运行时之外的代码和数据进行互操作。通过有效地调用外部函数(即JVM外部的代码)和安全地访问外部内存(即不由JVM管理的内存),API使Java程序能够调用本机库并处理本机数据,而不会像JNI那样脆弱和危险。这是一个预览版 API。
2、jdk21引入了MemorySegment内存段,和Arena竞技场
3、jdk21为什么要这么做? 答:java开发人员在访问一种重要的非Java资源时依然很困难
4、使用关键字创建的对象存储在 JVM 的堆中,当不再需要时,它们将受到垃圾回收。但是,垃圾收集的成本和不可预测性对于性能关键型库(如Tensorflow,Ignite,Lucene和Netty)来说是不可接受的。他们需要将数据存储在堆外,存储在堆外内存中,这些内存是他们自己分配和解除分配的。对堆外内存的访问还允许通过将文件直接映射到内存中来序列化和反序列化数据,例如 mmap,new
5、java平台历来提供两个用来访问堆外内存的api:
ByteBuffer Api:
提供直接字节缓冲区,这些缓冲区是由固定大小的堆外内存区域支持的 Java 对象。但是,区域的最大大小限制为 2 GB,读取和写入内存的方法基本且容易出错,仅提供对基元值的索引访问。更严重的是,仅当缓冲区对象被垃圾回收时,才会释放支持直接字节缓冲区的内存,这是开发人员无法控制的。缺乏对及时释放的支持使得 API 不适合使用 Java 进行系统编程。ByteBuffer
6、sun.misc.Unsafe Api:
提供对堆内存的低级别访问,该访问也适用于堆外内存。使用速度很快(因为它的内存访问操作是由 JVM 固有的),允许巨大的堆外区域(理论上高达 16 EB),并提供对释放的细粒度控制(因为可以随时调用)。但是,此编程模型很弱,因为它给了开发人员太多的控制。长时间运行的服务器应用程序中的库将随着时间的推移分配堆外内存的多个区域并与之交互;一个区域中的数据将指向另一个区域中的数据,并且必须以正确的顺序释放区域,否则悬而未决的指针将导致释放后使用错误。缺乏对安全释放的支持使得 API 不适合用 Java 进行系统编程。Unsafe
Unsafe::freeMemory
Unsafe
7、总之,复杂的客户端应该得到一个 API,它可以分配、操作和共享堆外内存,具有与堆内存相同的流动性和安全性。这样的 API 应该在可预测的释放需求与防止可能导致 JVM 崩溃或更糟糕的静默内存损坏的不合时宜的释放之间取得平衡。
8、外文函数
8.1、JNI从java1.1开始就支持调用本机函数(外部函数),但是不足点也多
Java API(方法)、派生自 Java API 的 C 头文件以及调用感兴趣的本机库的 C 实现。Java 开发人员必须跨多个工具链工作,以保持依赖于平台的工件同步,这在本机库快速发展时尤其繁重。native
JNI 只能与用语言(通常是 C 和 C++)编写的库进行互操作,这些库使用构建 JVM 的操作系统和 CPU 的调用约定。方法不能用于调用使用不同约定的语言编写的函数。native
JNI 不协调 Java 类型系统与 C 类型系统。Java 中的聚合数据用对象表示,但 C 中的聚合数据用结构表示,因此传递给方法的任何 Java 对象都必须由本机代码费力地解压缩。例如,考虑 Java 中的记录类:将对象传递给方法将需要本机代码使用 JNI 的 C API 从对象中提取字段(例如,和)。因此,Java 开发人员有时会将数据平展为单个对象(例如,字节数组或直接字节缓冲区),但更常见的是,由于通过 JNI 传递 Java 对象很慢,他们使用 API 分配堆外内存并将其地址作为方法传递给方法 - 这使得 Java 代码非常不安全!
9、内存段是由位于堆外或堆上的连续内存区域支持的抽象,分段的时间边界由用于分配分段的竞技场确定。
最简单的竞技场是全球竞技场,它提供了无限的生命周期:它永远活着。
自动竞技场提供有限的生存期:可以访问由自动竞技场分配的段,直到 JVM 的垃圾回收器检测到内存段不可访问,此时支持该段的内存区域被解除分配。
受限竞技场提供有限且确定的生存期:从客户端打开竞技场到客户端关闭竞技场,它一直处于活动状态。在受限竞技场中分配的内存段只能在竞技场关闭之前访问
本人先举个MemorySegment内存段,和Arena竞技场的例子吧
自由竞技场
package org.example.ass;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.ValueLayout.JAVA_DOUBLE;
class WindowsSharedMemory{
public static void main(String[] args) {
MemorySegment point= Arena.ofAuto().allocate(8*2);
point.set(JAVA_DOUBLE,0,3d);
point.set(JAVA_DOUBLE,8,4d);
System.out.println(point.get(JAVA_DOUBLE, 0));
}
这里我使用的是自由竞技场来创建的,也就是说当这个MemorySegment不可达时,该内存区域被解除分配
这里要注意一点,我是在0号位置set 3d占了8个字节,取的时候,就是在0号位置,取8个字节,这个JAVA_DOUBLE是所占的单位,因为double一般是8个字节
受限竞技场(错误代码),我之前说什么了?在受限竞技场中分配的内存段只能在竞技场关闭之前访问,我故意出个错误的代码
package org.example.ass;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.ValueLayout.JAVA_DOUBLE;
class WindowsSharedMemory{
public static void main(String[] args) {
MemorySegment point=null;
try ( Arena allocate= Arena.ofConfined()){
point = allocate.allocate(8 * 2);
point.set(JAVA_DOUBLE,0,3d);
point.set(JAVA_DOUBLE,8,4d);
}
System.out.println(point.get(JAVA_DOUBLE, 0));
}
全球竞技场我就不提例子了,因为太简单了,这个竞技场永远存活,把上面的例子中的.ofConfined()改为.global()就好了,运行结果:
没错又是报错,不过你看一下报错信息:试图关闭一个不可关闭的会话
这个是由于try ( 这里面运行完会自动调用关闭的那个方法){} 造成的,我就是故意举出错误例子,让你们深刻理解
10、为了应对c语言的结构体,例如:
Point
Point
Point.x
Point.y
struct Point {
int x;
int y;
} pts[10];
那我们如何在java的内存段里面搞呢?
openjdk引入了一个类,叫内存布局Memorylayout
package org.example.ass;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.MemoryLayout.sequenceLayout;
import static java.lang.foreign.ValueLayout.JAVA_DOUBLE;
class WindowsSharedMemory{
public static void main(String[] args) {
WindowsSharedMemory windowsSharedMemory=new WindowsSharedMemory();
windowsSharedMemory.play();
}
public void play(){
SequenceLayout POINT_2D =sequenceLayout(10, MemoryLayout.structLayout(
JAVA_DOUBLE.withName("x"),
JAVA_DOUBLE.withName("y")
));
VarHandle xHandle // (MemorySegment, long) -> int
= POINT_2D.varHandle(MemoryLayout.PathElement.sequenceElement(),
MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle // (MemorySegment, long) -> int
= POINT_2D.varHandle(MemoryLayout.PathElement.sequenceElement(),
MemoryLayout.PathElement.groupElement("y"));
MemorySegment segment = Arena.ofAuto().allocate(POINT_2D);
for (int i = 0; i < POINT_2D.elementCount(); i++) {
xHandle.set(segment,
/* index */ (long) i,
/* value to write */ i); // x
yHandle.set(segment,
/* index */ (long) i,
/* value to write */ i); // y
}
for (int i = 0; i < POINT_2D.elementCount(); i++) {
System.out.println(xHandle.get(segment,
/* index */ (long) i)); // x
System.out.println(yHandle.get(segment,
/* index */ (long) i)); // y
}
}
运行结果:
区段分配器
当客户端使用堆外内存时,内存分配通常是一个瓶颈。因此,FFM API 包括一个分段分配器抽象,用于定义分配和初始化内存段的操作。为方便起见,Arena 类实现了该接口,以便可以使用 arenas 来分配本机段。换句话说,是灵活分配和及时释放堆外内存的“一站式商店”:SegmentAllocator
Arena
package org.example.ass;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.MemoryLayout.sequenceLayout;
import static java.lang.foreign.ValueLayout.JAVA_DOUBLE;
import static java.lang.foreign.ValueLayout.JAVA_INT;
class WindowsSharedMemory{
public static void main(String[] args) {
try (Arena offHeap = Arena.ofConfined()) {
MemorySegment nativeArray = offHeap.allocateArray(ValueLayout.JAVA_INT,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
MemorySegment nativeString = offHeap.allocateUtf8String("Hello!");
System.out.println(nativeArray.get(JAVA_INT, 0));
System.out.println(nativeArray.get(JAVA_INT, 4));
System.out.println(nativeArray.get(JAVA_INT, 8));
System.out.println(nativeArray.get(JAVA_INT, 12));
System.out.println(nativeString.getUtf8String(0));
}
}
运行结果: