FFM
Foreign Function and Memory API,外部函数和堆外内存API
这里将用JDK22的语法来介绍FFM,我们可以基于此非常简单地调用非JAVA库和使用堆外内存
堆内存是 Java 堆中的内存,是垃圾收集器管理的内存区域。Java 对象驻留在堆中。应用程序运行时,堆可以增大或缩小。当堆已满时,将执行垃圾收集:JVM 识别不再使用的对象(无法访问的对象)并回收其内存,为新分配腾出空间。
堆外内存是 Java 堆之外的内存。要从 Java 应用程序调用其他语言(例如 C)的函数或方法,其参数必须位于堆外内存中。与堆内存不同,堆外内存在不再需要时不受垃圾回收的影响。您可以控制如何以及何时释放堆外内存
1 Arena和MemorySegment
通过arena来分配一个MemorySegment对象并控制其生命周期,MemorySegment对象可以直接与堆外内存交互
MemorySegment是Java对象被分配在堆内存中,但可以操作堆外内存,后文会认为MemorySegment与堆外内存是等价的,而arean就是MemorySegment的管理者
通过Arena接口提供的方法来创建一个arena,并使用arena分配一个MemorySegment
Arena arena = Arena.ofConfined()
MemorySegment nativeText = arena.allocateFrom("hello");
arena都有作用域,这决定了其创建的堆外内存MemorySegment何时被释放,并且不再有效,只有arena作用域仍然有效或者处于活动状态时,才能操作堆外内存
arena分为几类,比如上面创建的arena就是一个confined arena
-
confined arena
有限的arena
- confined arena提供了有限且确定的生命周期,也就是从创建arena对象开始到调用close方法结束
- confined arena有
属主线程,通常是创建该arena的线程,只有属主线程能使用confined arena分配的内存段,非属主线程关闭confined arena将会抛出异常
-
shared arena
共享的arena,通过下面代码创建
Arena arena = Arena.ofShared();shared arena没有属主线程,任何线程都能访问shared arena创建的内存段,并且能够关闭shared arena,关闭操作是原子和线程安全的
-
automatic arena
由垃圾收集器管理的arena,通过下面代码创建
Arena arena = Arena.ofAuto();任何线程都能访问automatic arena分配的内存段,由于automatic arena是被垃圾收集器自动管理的,所以它的close不用我们关心,如果其调用close,会抛出异常
-
global arena
全局的arena,一旦创建就一直存在不会被销毁,通过下面代码创建
Arena arena = Arena.global();任何线程都能访问global arena分配的内存段,并且这些内存段永远不会被释放,调用其close方法会抛出异常
下面代码使用confined arena分配了一个存储Java字符串的堆外内存,然后打印堆外内存的内容
try块执行完毕后arena被close,与之关联的堆外内存被销毁
String s = "hello FFM";
try (Arena arena = Arena.ofConfined()) {
// 分配堆外内存
MemorySegment nativeText = arena.allocateFrom(s);
//一个字节一个字节地访问堆外内存
for (int i = 0; i < s.length(); i++ ) {
//MemorySegment的get方法参数:读取的数据大小,从指定的索引位置开始读取
System.out.print((char)nativeText.get(ValueLayout.JAVA_BYTE, i));
}
}
//离开try块,arena被close,堆外内存被销毁
下面几节会详细介绍上面这个实例代码
1.1 分配堆外内存并写数据
SegmentAllocator是个接口,翻译为段分配器,也就是内存段(堆外内存)分配器,包含了分配堆外内存和向里面写数据的方法
Arena接口继承了SegmentAllocator,上面的使用Arena分配堆外内存的方法,本质就是来自SegmentAllocator的方法
//申请一块堆外内存,将hello FFM写入堆外内存
MemorySegment nativeText = arena.allocateFrom("hello FFM");
//从nativeText索引位置为0的地方读取一个字节:72
nativeText.getAtIndex(ValueLayout.JAVA_BYTE, 0);
SegmentAllocator::allocateUtf8String方法,能将Java字符串转为UTF8编码,并以'\0'结尾的结尾的C语言风格的字符串,然后将这个字符串写入分配的堆外内存
该方法同时执行了分配堆外内存和写数据的操作
1.2 读写堆外内存数据
之前说过MemorySegment可以视为堆外内存,但实际上MemorySegment只是个接口,其实现类隐藏了操作堆外内存的细节
MemorySegment包含了一系列读写堆外内存的方法,每个方法都以ValueLayout作为参数,ValueLayout与Java的基本数据类型是对应的,并以此构建访问时的内存布局
比如ValueLayout.JAVA_BYTE对应着Java中的byte,表示以字节为单位进行访问
//向nativeText索引位置为0的地方写入一个字节
nativeText.setAtIndex(ValueLayout.BYTE, 0, (byte)73);
同理还有
- ValueLayout.JAVA_BOOLEAN:与 Java boolean的大小相同,字节对齐设置为 1
- ValueLayout.JAVA_CHAR:与 Java char的大小相同,字节对齐设置为 2
- ValueLayout.JAVA_SHORT:与 Java short的大小相同,字节对齐设置为 2
- ValueLayout.JAVA_INT:与 Java int的大小相同,字节对齐设置为 4
- ValueLayout.JAVA_LONG:与 Java long的大小相同,(平台相关)字节对齐设置为ADDRESS. byteSize()
- ValueLayout.JAVA_FLOAT:与 Java float的大小相同,字节对齐设置为 4
- ValueLayout.JAVA_DOUBLE:与 Java double的大小相同,(平台相关)字节对齐设置为ADDRESS. byteSize()
- ValueLayout.ADDRESS:平台相关,32系统中其大小为32位宽,64系统为64位宽,通常用来保存指针
上面对应Java类型的内部布局是有符号,对应还有无符号比如ValueLayout.JAVA_CHAR_UNALIGNED等
ValueLayout有以下特点
- 与对应Java类型所占内存相同
- 分配的地址处于字节对齐的整数倍,比如字节对齐设置为 1,表示堆外内存分配在8bit整数倍的内存地址处
- 字节顺序设置为
ByteOrder.nativeOrder(),表示系统可以按从最高有效位到最低有效位(大端序)或从最低有效位到最高有效位(小端序)对多字节值的字节进行排序
1.3 close
当arena被close时,arena的作用域不再有效,与作用域关联的所有堆外内存,也就是通过该arena分配的堆外内存都会失效并被回收
如果此时访问这些堆外内存,会抛出异常IllegalStateException
MemorySegment nativeText;
try (Arena arena = Arena.ofConfined()) {
nativeText = arena.allocateFrom("My String");
}
//尝试访问失效的堆外内存
System.out.print((char)nativeText.get(ValueLayout.JAVA_BYTE, 1));
2 向下调用
在Java中调用C语言函数,过去这种操作是很繁琐的,但使用FFM可以很简单地实现这个需求
比如Windows操作系统自带的c语言函数strlen长这样
//接收一个字符串,并返回该字符串长度
size_t strlen(const char *s);
在Java中调用这个函数
// 获取链接器和符号查找器,用于查找c函数
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
// 找到C库中的strlen函数,strlenSymbol表示c语言的strlen函数在内存中的位置
MemorySegment strlenSymbol = stdlib.find("strlen").get();
//创建一个下调用方法句柄,它将strlen函数作为本地方法调用,strlenMH等价于strlen函数
//FunctionDescriptor.of(JAVA_LONG, ADDRESS)是c函数签名,表示该函数接收一个地址ADDRESS作为参数,返回一个JAVA_LONG结果
MethodHandle strlenMH = linker.downcallHandle(strlenSymbol,
FunctionDescriptor.of(ValueLayout.JAVA_LONG,ValueLayout.ADDRESS));
// 分配一个内存段来存储一个字符串,作为c函数的参数
Arena arena = Arena.ofConfined();
MemorySegment strSegment = arena.allocateFrom("Hello, World!");
// 调用strlen函数来获取字符串的长度,就像普通反射一样
long stringLength = (long) strlenMH.invokeExact(strSegment);
// 输出字符串长度
System.out.println("String length: " + stringLength);
arena.close
为什么invokeExact调用c函数时,不直接new一个String作为其参数,而是要使用堆外内存呢?
由于GC的缘故,堆内的对象的内存地址一直在变化,所以不能使用堆内内存,只能使用不会变换的堆外内存
上面只是演示,正确使用Arena要配合try-catch-resouce哦
下面详细介绍下上诉实例代码
2.1 Native Linker
本地链接器,提供了对遵循Java运行时平台的调用约定的库的访问,这些库被称为"native libraries"
比如上面的strlen就是一个native libraries,使用下面代码创建一个本地链接器
Linker linker = Linker.nativeLinker();
2.2 定位外部函数地址
想要调用外部函数,最基础的是你总的先找到这个外部函数吧,所以下面代码定义得到外部函数的内存地址
//根据指定的符号,创建一个库查找器,libc.so.6是linuxC标准库的文件名
SymbolLookup stdLib = SymbolLookup.libraryLookup("libc.so.6", Arena.ofConfined());
//使用库查找器查找对应函数,并返回该函数的内存地址
MemorySegment strlen_addr = stdLib.find("strlen").get();
这代码跟上面的实例代码有区别,因为strlen是 C 标准库的一部分,可以直接使用defaultLookup创建默认查找器,这会查找常用库(包括 C 标准库)中的符号
//使用默认查找器
SymbolLookup stdLib = linker.defaultLookup();
MemorySegment strlen_addr = stdLib.find("strlen").get();
2.3 外部函数描述符
同Java中的反射调用普通方法一样,调用外部函数,必须知道外部函数的签名,比如参数类型、返回值类型,于是就有了FunctionDescriptor
//创建一个外部函数的描述符,该函数返回long,参数是一个地址,也就是指向字符串的指针
FunctionDescriptor strlen_sig =
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
2.4 MethodHandle
外部函数的地址有了,描述符有了,就可以根据这二者创建MethodHandle,也就是外部函数的Downcall Handle,即向下调用句柄
MethodHandle strlen = linker.downcallHandle(strlen_addr, strlen_sig);
有了MethodHandle,就可以像反射那样直接调用外部函数了
//创建c函数的参数
MemorySegment strSegment = arena.allocateFrom("Hello, World!");
return (long)strlen.invokeExact(nativeString)
3 向上调用
将 Java 代码作为函数指针传递给外部函数,这里使用C函数库提供的排序函数对Java中的数组进行排序
3.1 定义外部函数
qsort是标准 C 库函数,能对数组元素进行排序
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
没有返回值,且接受四个参数
- base:指向要排序的数组的第一个元素的指针
- nmemb:数组中元素的数量
- size:数组中每个元素的大小(以字节为单位)
- compar:指向比较两个元素的函数的指针
先为该qsort创建MethodHandle实例
Linker linker = Linker.nativeLinker();
//Java可以通过qsort调用该外部函数,也就是向下调用,不过这里演示的是向上调用
MethodHandle qsort = linker.downcallHandle(
linker.defaultLookup().find("qsort").get(),
FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS)
);
该函数返回值为void,所以其描述符对象可以调用ofVoid方法创建,方法参数跟函数参数一致
3.2 定义Java代码
向上调用,也就是将Java代码作为函数指针传递给外部函数,所以需要先准备一段Java代码
class Qsort {
static int qsortCompare(MemorySegment elem1, MemorySegment elem2) {
return Integer.compare(
elem1.get(ValueLayout.JAVA_INT, 0), elem2.get(ValueLayout.JAVA_INT, 0)
);
}
}
这段Java代码同样可以作为MethodHandle方法句柄
创建Java代码的方法句柄不是通过本地链接器,而是通过MethodHandles
//findStatic表示查找Java静态方法,三个参数:所在类,静态方法名称,静态方法签名(返回值,参数列表)
MethodHandle comparHandle = MethodHandles.lookup().findStatic(
Qsort.class, "qsortCompare", MethodType.methodType(
int.class, MemorySegment.class, MemorySegment.class
)
);
3.3 创建Java代码的函数指针
上面将Java方法封装成了MethodHandle,基于MethodHandle为Java代码创建函数指针,作为外部函数的参数
//创建Java代码的描述符
FunctionDescriptor qsortCompareDesc = FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_INT),
ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_INT)
);
//为Java代码创建对应的函数指针
MemorySegment compareFunc = linker.upcallStub(comparHandle, qsortCompareDesc, Arena.ofAuto());
upcallStub有三个参数:
- Java代码的方法句柄
- Java代码的描述符
- 与函数指针关联的区域,Arena.ofAuto()创建一个由垃圾收集器自动管理的新区域
3.4 进行排序
上面三个步骤满足了向上调用的先决条件,这里开始执行向上调用
try (Arena arena = Arena.ofConfined()) {
//未排序的数组
int[] unsortedArray = new int[]{0, 9, 3, 4, 6, 5, 1, 8, 2, 7};
//将未排序的数组写入堆外内存
MemorySegment array = arena.allocateFrom(ValueLayout.JAVA_INT, unsortedArray);
//调用外部函数开始排序
qsort.invoke(
array, //要排序的数组的第一个元素的指针
(long) unsortedArray.length, //数组中元素的数量
ValueLayout.JAVA_INT.byteSize(), //数组中每个元素的大小
compareFunc); //指向比较两个元素的函数的指针,也就是Java代码的函数指针
int[] sorted = array.toArray(ValueLayout.JAVA_INT);
for (int num : sorted) {
System.out.print(num + " ");
}
}
由此可见,其实向上调用本质是一种特殊的向下调用,调用外部函数时,将Java代码作为参数传递给外部函数,是为向上调用
4 返回指针的外部函数
C语言中很多函数的返回值是一个指针,比如C 标准库函数void *malloc(size_t)申请指定字节大小的内存空间,并返回指向该块内存的起始指针(操作系统会记录每个指针占用的大小)
Java中调用这种外部函数时,只知道指针本身所在的那一块内存空间,而不知道指针指向的内存的大小和生命周期
为了避免混淆,FFM使用零长度的内存段来表示这种指针
Linker linker = Linker.nativeLinker();
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
MethodHandle malloc = linker.downcallHandle(
malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
//上面代码得到了C语言的malloc函数所在的内存地址,并以此创建MethodHandle
//下面代码调用malloc函数,分配一块12个字符大小的内存并返回内存的指针segment
MemorySegment segment = (MemorySegment) malloc.invokeExact(
ValueLayout.JAVA_CHAR.byteSize() * 12
);
//得到MemorySegment占用字节,发现是0,说明是一个指针
System.out.println(segment.byteSize());
//读取零长度的segment会抛出IndexOutOfBoundsException异常
segment.get(ValueLayout.JAVA_BYTE, 0);
FFM API 使用零长度内存段来表示以下内容:
- 从外部函数返回的指针
- 外部函数传递给上行调用的指针
- 从内存段读取的指针
零长度MemorySegment与始终处于活动状态的新作用域相关联。因此,即使您无法直接访问零长度内存段,也可以将它们传递给其他接受指针的外部函数
如果想要在Java中使用零长度MemorySegment指向的内存区域里的数据怎么办呢?
可以使用MemorySegment::reinterpret,该方法有三个参数
- 内存段占用字节数
- 内存段关联的arena
- 当arena关闭时执行的回调
第三个参数一般是指:回收之前申请的内存,因此需要用到C 标准库函数void free(void *ptr)函数
//定位到free函数
var free_addr = linker.defaultLookup().find("free").orElseThrow();
MethodHandle free = linker.downcallHandle(
free_addr,
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
);
//回调函数,调用free函数是否内存段s指针指向的内存
Consumer<MemorySegment> cleanup = s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
//调用reinterpret,将cleanup以及nativeText注册给arena,将来arena关闭时,就会调用cleanup
MemorySegment nativeText = segment.reinterpret(
ValueLayout.JAVA_CHAR.byteSize() * 12, arena, cleanup);
然后就可以使用nativeText往之前申请的12个字符大小的内存中写入数据
String str = "My string!!";
for (int i = 0; i < str.length(); i++ ) {
//将str字符串11个字符写入到之前分配的内存段中
nativeText.setAtIndex(ValueLayout.JAVA_CHAR, i, str.charAt(i));
}
//再写入一个结束字符
nativeText.setAtIndex(ValueLayout.JAVA_CHAR, str.length(), Character.MIN_VALUE);
读取写入的字符
//只读取11个字符,最后一个结束符就必要读取了
for (int i = 0; i < str.length(); i++ ) {
System.out.print((char)nativeText.getAtIndex(ValueLayout.JAVA_CHAR, i));
}
5 内存布局和结构化访问
Memory Layouts and Structured Access
5.1 没有内存布局时
前面例子都使用了allocateUtf8String来申请一块堆外内存并写入字符串数据,也可以使用Arena ::allocate直接申请一块指定字节大小的内存
比如对于下面的结构体数组
struct Point {
int x;
int y;
} pts[10];
使用下面的Java代码来申请一块堆外内存
//int占4个字节,而每个结构体有两个int,同时又存在10个结构体,所以分配的字节数是2 * 4 * 10,字节对齐为1
MemorySegment segment = arena.allocate((long)(2 * 4 * 10), 1);
然后向堆外内存写入数据,也就是给每个结构体的每个成员赋值
//循环10次,对应10个结构体
for (int i = 0; i < 10; i++) {
//给当前结构体的x赋值,也就是写入一个4字节的int数据
segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2), i);
//给当前结构体的y赋值,也就是写入一个4字节的int数据,每个结构体的y的内存地址比x多4个字节
segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1, i * 10);
}
然后读取每个结构体的值
for (int i = 0; i < 10; i++) {
int xVal = segment.getAtIndex(ValueLayout.JAVA_INT, (i * 2));
int yVal = segment.getAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1);
System.out.println("(" + xVal + ", " + yVal + ")");
}
手动计算要分配的总字节数和结构体每个成员的地址偏移量,数据量一多,复杂度就指数增加,下面介绍使用内存布局和结构化访问来解决这个问题
5.1 使用内存布局时
使用MemoryLayout来解决这个问题,也就是这一章要介绍的知识,可以使用MemoryLayout分配结构体内存
//下面代码表示分配10个序列,序列元素是结构体,结构体有两个int类型成员x、y
SequenceLayout ptsLayout = MemoryLayout.sequenceLayout(10,
MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")));
MemoryLayout帮我们计算出结构体数组占用的字节,基于此分配内存
//分配结构体占用的内存,也就是上面的2 * 4 * 10字节
MemorySegment segment = arena.allocate(ptsLayout);
//打印结果是:80,表示分配了80字节的堆外内存
System.out.println(segment.byteSize());
然后创建两个用于获取内存地址偏移量的内存访问 VarHandle,VarHandle是对变量或参数定义的变量系列的动态强类型引用,包括静态字段、非静态字段、数组元素或堆外数据结构的组件
VarHandle xHandle
= ptsLayout.varHandle(MemoryLayout.PathElement.sequenceElement(),
MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle
= ptsLayout.varHandle(MemoryLayout.PathElement.sequenceElement(),
MemoryLayout.PathElement.groupElement("y"));
PathElement.sequenceElement()表示我们要访问的是序列中的元素,PathElement.groupElement("x")表示我们要访问的是名为x的字段,可以通过xHandle简单地操作某个结构体的x成员,而不用像之前那样计算偏移量
然后使用VarHandle操作每个结构体的成员,我们无需计算内存地址偏移量
//给每个结构体成员赋值
for (int i = 0; i < ptsLayout.elementCount(); i++) {
//四个参数:要被赋值的堆外内存、数组的起始位置、给第几个结构体赋值、赋值数据
xHandle.set(segment, 0L, (long) i, i);
yHandle.set(segment, 0L, (long) i, i * 10));
}
//得到每个结构体的数据
for (int i = 0; i < ptsLayout.elementCount(); i++) {
//三个参数:要被赋值的堆外内存、数组的起始位置、得到第几个结构体的数据
int xVal = (int) xHandle.get(segment, 0L, (long) i);
int yVal = (int) yHandle.get(segment, 0L, (long) i);
System.out.println("(" + xVal + ", " + yVal + ")");
}
6 捕获外部函数的错误
在执行外部函数过程中,可能会产生错误,怎么在Java中获取这些错误信息呢?
C标准库的函数将错误信息存放在C标准库宏error中,因此可以通过访问这个宏判断是否有错误信息
调用外部函数的方法Linker::downcallHandle包含了一个varargs参数能够设置Linker选项,这些选项都是Linker.Option类型,比如Linker.Option.captureCallState(String...),调用调用外部函数后立即保存执行状态,可以使用它来捕获某些线程局部变量。当与errno字符串一起使用时,它会捕获error这个宏的值
Linker.Option ccs = Linker.Option.captureCallState("errno");
StructLayout capturedStateLayout = Linker.Option.captureStateLayout();
//errno是一个整数,用于存储错误码,使用VarHandle来方便地访问这个错误码
VarHandle errnoHandle = capturedStateLayout.varHandle(MemoryLayout.PathElement.groupElement("errno"));
C 标准库有一个log函数,用于计算对数,如果传入错误的参数比如-1就会把error置为33,下面代码得到这个函数
Linker linker = Linker.nativeLinker();
SymbolLookup stdLib = linker.defaultLookup();
MethodHandle log = linker.downcallHandle(
stdLib.find("log").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE),
ccs);
很多人不知道error为33是什么意思,于是通过C 标准库另一个函数`strerror返回error错误码的提示信息
MethodHandle strerror = linker.downcallHandle(
stdLib.find("strerror").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT));
然后调用log函数,根据返回值判断函数是否发生异常,如果发生异常就通过errnoHandle捕获错误码,并调用strerror得到错误码的提示信息
try (Arena arena = Arena.ofConfined()) {
//分配一块堆外内存用于存储捕获的状态
MemorySegment capturedState = arena.allocate(capturedStateLayout);
//调用log函数,此时就需要传两个参数,一个是捕获状态使用的内存,一个是函数的参数
double r = (double) log.invokeExact(capturedState, -1.0);
//打印对数计算结果,如果计算失败,则r为NaN
System.out.println(r);
if (Double.isNaN(r)) {
//进入这里说明log函数发生错误,使用errnoHandle得到错误码
int errno = (int) errnoHandle.get(capturedState, 0L);
System.out.println("errno: " + errno); // 33
//然后根据错误码调用strerror函数得到错误码对应的字符串
String errrorString = ((MemorySegment) strerror.invokeExact(errno))
.reinterpret(Long.MAX_VALUE).getString(0);
System.out.println("errno string: " + errrorString); // Domain error
}
}
7 MemorySegment的切分
7.1 切片分配器
通过SegmentAllocator::slicingAllocator(MemorySegment)构造一个切片分配器
Arena arena = Arena.ofConfined();
//表示一块可以存储60个int数字的内存空间
SequenceLayout SEQUENCE_LAYOUT = MemoryLayout.sequenceLayout(60L, ValueLayout.JAVA_INT);
//使用SequenceLayout分配堆外内存
MemorySegment segment = arena.allocate(SEQUENCE_LAYOUT);
//从MemorySegment中构件SegmentAllocator
SegmentAllocator allocator = SegmentAllocator.slicingAllocator(segment);
然后使用切片分配器将大的MemorySegment切分成10个切片
MemorySegment[] s = new MemorySegment[10];
for (int i = 0 ; i < 10 ; i++) {
//每个切片占5个int数字的内存,并给每个切片赋予初始值1, 2, 3, 4, 5
s[i] = allocator.allocateFrom(ValueLayout.JAVA_INT, 1, 2, 3, 4, 5);
}
读取每个切片里的内容
for (int i = 0 ; i < 10 ; i++) {
int[] intArray = s[i].toArray(ValueLayout.JAVA_INT);
System.out.println(Arrays.toString(intArray));
}
读取大MemorySegment里的值,发现是1、2、3、4、5、1、2、3、4、5......共60个,所以可以知道切片并不是创建了一个新的MemorySegment,而是在原本的MemorySegment上进行逻辑切分
System.out.println(Arrays.toString(segment.toArray(ValueLayout.JAVA_INT)));
7.2 内存分段
通过MemorySegment::asSlice(long, long)可以获取MemorySegment内存段中任意位置、任意大小的内存段切片
String s = "abcdefghijklmnopqrstuvwxyz";
char c[] = s.toCharArray();
//将字符数组写入堆外内存
MemorySegment textSegment = MemorySegment.ofArray(c);
进行切片
//得到每个字符的字节数
long b = ValueLayout.JAVA_CHAR.byteSize();
long firstLetter = 5;
long size = 6;
//从原本的MemorySegment中第5个字符开始切片,切片长度为6个字符
MemorySegment fghijk = textSegment.asSlice(5 * b, 6 * b);
读取切片
//打印:fghijk
for (int i = 0; i < size; i++) {
System.out.print((char)fghijk.get(ValueLayout.JAVA_CHAR, i*b));
}
7.3 切片流
通过MemorySegment::elements(MemoryLayout)可以将MemorySegment切分成指定内存布局的MemorySegment流,
//得到100个长度的随机int数组
int[] numbers = new Random().ints(100, 0, 1000).toArray();
try (Arena arena = Arena.ofShared()) {
//分配100个int长度的堆外内存
SequenceLayout SEQUENCE_LAYOUT = MemoryLayout.sequenceLayout(
(long)100, ValueLayout.JAVA_INT);
MemorySegment segment = arena.allocate(SEQUENCE_LAYOUT);
MemorySegment.copy(numbers, 0, segment, ValueLayout.JAVA_INT, 0L, 100);
//将MemorySegment切分成100片,每片占一个int长度,然后多线程并行读取每个切片的值并计算综合
int sum = segment.elements(ValueLayout.JAVA_INT).parallel()
.mapToInt(s -> s.get(ValueLayout.JAVA_INT, 0))
.sum();
System.out.println(sum);
}