兄弟们,鹏磊今天来聊聊 Java 22 里的外部函数和内存 API(Foreign Function and Memory API,简称 FFM API)这个新特性,这玩意儿是 JEP 454 引入的,专门用来替代 JNI(Java Native Interface)的。说实话,用 JNI 这么多年了,虽然功能强大,但确实有不少坑,写起来复杂、容易出错、性能也不咋地,而且容易导致内存泄漏、段错误啥的。FFM API 就是为了解决这些问题来的,它提供了一种更安全、更简单、更高效的方式来调用原生函数和操作原生内存。
FFM API 是 Java 22 的预览特性,它让 Java 程序能够与 Java 运行时之外的代码和数据互操作。核心思想是:通过类型安全的内存访问和函数调用,让 Java 代码能够安全地调用 C 函数、操作 C 数据结构,而不需要写 JNI 代码。这玩意儿特别适合需要调用系统库、第三方 C 库、或者需要高性能内存操作的场景。
为什么需要 FFM API
先说说 JNI 的问题。JNI 虽然用起来功能强大,但确实有不少坑:
// JNI 的典型用法(需要写 C 代码)
// Java 端
public class NativeExample {
static {
System.loadLibrary("native"); // 加载原生库
}
// 声明原生方法
public native int add(int a, int b); // 调用原生函数
public native void processData(byte[] data); // 处理数据
}
// C 端(需要单独写 C 代码)
// JNIEXPORT jint JNICALL Java_NativeExample_add(JNIEnv *env, jobject obj, jint a, jint b) {
// return a + b; // 实现原生函数
// }
// 问题1:需要写 C 代码,开发效率低
// 问题2:容易出错,类型转换复杂
// 问题3:内存管理复杂,容易泄漏
// 问题4:性能开销大,JNI 调用有额外开销
// 问题5:调试困难,跨语言调试很麻烦
FFM API 就是为了解决这些问题。它完全用 Java 代码就能调用原生函数,不需要写 C 代码,而且类型安全、内存安全,性能也更好。
核心概念
FFM API 的核心概念包括:
- MemorySegment(内存段):表示一块连续的内存区域,可以是堆内或堆外内存
- Arena(内存域):管理内存段生命周期的工具,确保内存正确释放
- MemoryLayout(内存布局):描述内存中数据的结构,比如结构体、数组等
- Linker(链接器):用于链接和调用原生函数
- FunctionDescriptor(函数描述符):描述原生函数的签名
基本用法:内存操作
分配和访问堆外内存
最基础的用法就是分配堆外内存,然后读写数据:
import java.lang.foreign.*; // 导入 FFM API 包
// 分配和访问堆外内存
public void basicMemoryOperation() {
// 使用 Arena 管理内存生命周期,try-with-resources 确保自动释放
try (Arena arena = Arena.ofConfined()) { // 创建受限制的内存域,只能在当前线程使用
// 分配 4 字节的堆外内存,用于存储一个整数
MemorySegment segment = arena.allocate(4); // 分配内存段
// 在内存段的偏移 0 处写入一个整数 42
segment.set(ValueLayout.JAVA_INT, 0, 42); // 写入整数值
// 从内存段的偏移 0 处读取整数
int value = segment.get(ValueLayout.JAVA_INT, 0); // 读取整数值
System.out.println("存储的值: " + value); // 输出:存储的值: 42
}
// Arena 关闭时,所有分配的内存自动释放,不用担心内存泄漏
}
这玩意儿的好处是,内存生命周期由 Arena 管理,作用域结束时自动释放,不用担心内存泄漏。而且类型安全,编译时就能检查类型错误。
分配字符串内存
分配字符串内存也很简单,可以用 allocateFrom 方法:
import java.lang.foreign.*; // 导入 FFM API 包
// 分配字符串内存
public void allocateString() {
try (Arena arena = Arena.ofConfined()) { // 创建内存域
// 分配 C 字符串(以 null 结尾的字符串)
MemorySegment cString = arena.allocateFrom("Hello, FFM!"); // 分配字符串内存,自动添加 null 结尾
// 可以读取字符串内容
String javaString = cString.getUtf8String(0); // 从偏移 0 开始读取 UTF-8 字符串
System.out.println("字符串内容: " + javaString); // 输出:字符串内容: Hello, FFM!
}
}
这个场景下,allocateFrom 会自动把 Java 字符串转换成 C 字符串(以 null 结尾),方便传给原生函数。
调用原生函数
调用标准库函数
调用原生函数是 FFM API 的核心功能。比如调用 C 标准库的 strlen 函数:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.MethodHandle; // 导入方法句柄类
// 调用 C 标准库的 strlen 函数
public void callStrlen() throws Throwable {
// 获取原生链接器,用于链接原生函数
Linker linker = Linker.nativeLinker(); // 创建原生链接器
// 获取标准库的符号查找器,用于查找函数
SymbolLookup stdlib = linker.defaultLookup(); // 获取默认符号查找器(通常是标准库)
// 查找 strlen 函数
MemorySegment strlenFunc = stdlib.find("strlen") // 查找函数符号
.orElseThrow(() -> new RuntimeException("找不到 strlen 函数")); // 如果找不到就抛异常
// 定义函数签名:long strlen(const char*)
// 返回值是 long(在 64 位系统上),参数是 const char*(内存地址)
FunctionDescriptor strlenDesc = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // 返回值类型:long
ValueLayout.ADDRESS // 参数类型:const char*(内存地址)
);
// 获取方法句柄,用于调用原生函数
MethodHandle strlen = linker.downcallHandle(strlenFunc, strlenDesc); // 创建下行调用句柄
try (Arena arena = Arena.ofConfined()) { // 创建内存域
// 分配 C 字符串
MemorySegment cString = arena.allocateFrom("Hello, FFM!"); // 分配字符串内存
// 调用 strlen 函数,传入字符串地址
long length = (long) strlen.invoke(cString); // 调用原生函数,获取字符串长度
System.out.println("字符串长度: " + length); // 输出:字符串长度: 12
}
}
这个例子展示了如何调用原生函数。关键是定义函数签名(返回值类型和参数类型),然后创建方法句柄,最后调用。
调用自定义原生函数
如果要用自己的原生库,需要先加载库,然后查找函数:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.MethodHandle; // 导入方法句柄类
// 调用自定义原生库的函数
public void callCustomNativeFunction() throws Throwable {
// 加载自定义原生库
System.loadLibrary("mylib"); // 加载名为 mylib 的原生库
Linker linker = Linker.nativeLinker(); // 创建链接器
// 查找自定义函数(需要知道函数名)
// 假设原生库中有一个函数:int add(int a, int b)
MemorySegment addFunc = SymbolLookup.loaderLookup() // 使用类加载器查找符号
.find("add") // 查找 add 函数
.orElseThrow(() -> new RuntimeException("找不到 add 函数")); // 如果找不到就抛异常
// 定义函数签名:int add(int a, int b)
FunctionDescriptor addDesc = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // 返回值类型:int
ValueLayout.JAVA_INT, // 第一个参数:int
ValueLayout.JAVA_INT // 第二个参数:int
);
// 创建方法句柄
MethodHandle add = linker.downcallHandle(addFunc, addDesc); // 创建下行调用句柄
// 调用函数
int result = (int) add.invoke(10, 20); // 调用 add(10, 20)
System.out.println("结果: " + result); // 输出:结果: 30
}
这个场景下,需要先用 System.loadLibrary 加载原生库,然后用 SymbolLookup.loaderLookup() 查找函数符号。
内存布局和结构化访问
定义结构体布局
FFM API 支持定义 C 结构体的内存布局,然后像访问 Java 对象一样访问结构体字段:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.VarHandle; // 导入变量句柄类
// 定义和访问 C 结构体
public void workWithStruct() {
try (Arena arena = Arena.ofConfined()) { // 创建内存域
// 定义 Point 结构体的内存布局
// 结构体包含两个 int 字段:x 和 y
StructLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"), // x 字段:int 类型
ValueLayout.JAVA_INT.withName("y") // y 字段:int 类型
);
// 为结构体分配内存
MemorySegment pointSegment = arena.allocate(pointLayout); // 分配足够的内存来存储结构体
// 获取字段的变量句柄,用于访问结构体字段
VarHandle xHandle = pointLayout.varHandle(PathElement.groupElement("x")); // 获取 x 字段的句柄
VarHandle yHandle = pointLayout.varHandle(PathElement.groupElement("y")); // 获取 y 字段的句柄
// 设置字段值
xHandle.set(pointSegment, 0, 10); // 在偏移 0 处设置 x = 10
yHandle.set(pointSegment, 0, 20); // 在偏移 0 处设置 y = 20
// 读取字段值
int x = (int) xHandle.get(pointSegment, 0); // 读取 x 字段的值
int y = (int) yHandle.get(pointSegment, 0); // 读取 y 字段的值
System.out.println("坐标: (" + x + ", " + y + ")"); // 输出:坐标: (10, 20)
}
}
这个例子展示了如何定义结构体布局,然后用 VarHandle 访问字段。关键是定义内存布局,然后获取字段的变量句柄。
定义数组布局
FFM API 还支持数组,可以用 SequenceLayout 定义数组布局:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.VarHandle; // 导入变量句柄类
// 定义和访问数组
public void workWithArray() {
try (Arena arena = Arena.ofConfined()) { // 创建内存域
// 定义 Point 结构体布局
StructLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"), // x 字段
ValueLayout.JAVA_INT.withName("y") // y 字段
);
// 定义包含 10 个 Point 的数组布局
SequenceLayout pointsLayout = MemoryLayout.sequenceLayout(10, pointLayout); // 定义数组布局
// 为数组分配内存
MemorySegment pointsSegment = arena.allocate(pointsLayout); // 分配足够的内存来存储数组
// 获取数组元素的字段句柄
// 需要指定数组索引和字段名
VarHandle xHandle = pointsLayout.varHandle(
PathElement.sequenceElement(), // 数组元素路径
PathElement.groupElement("x") // x 字段路径
);
VarHandle yHandle = pointsLayout.varHandle(
PathElement.sequenceElement(), // 数组元素路径
PathElement.groupElement("y") // y 字段路径
);
// 初始化数组
for (int i = 0; i < 10; i++) { // 遍历数组元素
xHandle.set(pointsSegment, 0L, (long) i, i); // 设置第 i 个元素的 x 字段为 i
yHandle.set(pointsSegment, 0L, (long) i, i * 10); // 设置第 i 个元素的 y 字段为 i*10
}
// 访问数组
for (int i = 0; i < 10; i++) { // 遍历数组元素
int x = (int) xHandle.get(pointsSegment, 0L, (long) i); // 读取第 i 个元素的 x 字段
int y = (int) yHandle.get(pointsSegment, 0L, (long) i); // 读取第 i 个元素的 y 字段
System.out.println("点 " + i + ": (" + x + ", " + y + ")"); // 输出坐标
}
}
}
这个例子展示了如何定义和访问结构体数组。关键是使用 PathElement.sequenceElement() 来指定数组元素路径。
复杂结构体示例
实际应用中,结构体可能更复杂,包含嵌套结构体、数组等:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.VarHandle; // 导入变量句柄类
// 定义复杂的结构体(包含嵌套结构体和数组)
public void workWithComplexStruct() {
try (Arena arena = Arena.ofConfined()) { // 创建内存域
// 定义 Point 结构体
StructLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"), // x 坐标
ValueLayout.JAVA_INT.withName("y") // y 坐标
);
// 定义 Rectangle 结构体,包含两个 Point 和一个 int 数组
StructLayout rectLayout = MemoryLayout.structLayout(
pointLayout.withName("topLeft"), // 嵌套结构体:左上角点
pointLayout.withName("bottomRight"), // 嵌套结构体:右下角点
MemoryLayout.sequenceLayout(4, ValueLayout.JAVA_INT).withName("colors") // int 数组:颜色值
);
// 分配内存
MemorySegment rectSegment = arena.allocate(rectLayout); // 分配结构体内存
// 获取字段句柄
VarHandle topLeftXHandle = rectLayout.varHandle(
PathElement.groupElement("topLeft"), // 嵌套结构体路径
PathElement.groupElement("x") // x 字段路径
);
VarHandle topLeftYHandle = rectLayout.varHandle(
PathElement.groupElement("topLeft"), // 嵌套结构体路径
PathElement.groupElement("y") // y 字段路径
);
VarHandle colorHandle = rectLayout.varHandle(
PathElement.groupElement("colors"), // 数组路径
PathElement.sequenceElement() // 数组元素路径
);
// 设置值
topLeftXHandle.set(rectSegment, 0, 10); // 设置左上角 x = 10
topLeftYHandle.set(rectSegment, 0, 20); // 设置左上角 y = 20
// 设置数组元素
for (int i = 0; i < 4; i++) { // 遍历颜色数组
colorHandle.set(rectSegment, 0L, (long) i, i * 64); // 设置颜色值
}
// 读取值
int x = (int) topLeftXHandle.get(rectSegment, 0); // 读取左上角 x
int y = (int) topLeftYHandle.get(rectSegment, 0); // 读取左上角 y
System.out.println("左上角: (" + x + ", " + y + ")"); // 输出坐标
}
}
这个例子展示了如何处理复杂的嵌套结构体。关键是使用路径元素来访问嵌套的字段。
内存管理:Arena 的使用
不同类型的 Arena
FFM API 提供了几种不同类型的 Arena,用于不同的内存管理场景:
import java.lang.foreign.*; // 导入 FFM API 包
// 不同类型的 Arena
public void differentArenas() {
// 1. Confined Arena:受限制的内存域,只能在创建它的线程使用
// 适合单线程场景,性能最好
try (Arena confinedArena = Arena.ofConfined()) { // 创建受限制的内存域
MemorySegment segment1 = confinedArena.allocate(100); // 分配内存
// 只能在这个线程使用 segment1
}
// 作用域结束时,内存自动释放
// 2. Shared Arena:共享的内存域,可以在多个线程使用
// 适合多线程场景,但需要手动关闭
Arena sharedArena = Arena.ofShared(); // 创建共享的内存域
try {
MemorySegment segment2 = sharedArena.allocate(100); // 分配内存
// 可以在多个线程使用 segment2
// 使用完后需要手动关闭
} finally {
sharedArena.close(); // 手动关闭,释放所有内存
}
// 3. Global Arena:全局内存域,内存不会被自动释放
// 适合需要长期存在的内存,但需要手动管理
Arena globalArena = Arena.global(); // 获取全局内存域
MemorySegment segment3 = globalArena.allocate(100); // 分配内存
// segment3 会一直存在,直到程序结束
// 注意:全局 Arena 的内存不会自动释放,需要谨慎使用
}
选择哪种 Arena 取决于使用场景:单线程用 ofConfined(),多线程用 ofShared(),长期存在的数据用 global()。
内存段的生命周期管理
内存段的生命周期由 Arena 管理,但也可以手动控制:
import java.lang.foreign.*; // 导入 FFM API 包
// 内存段的生命周期管理
public void memorySegmentLifecycle() {
Arena arena = Arena.ofShared(); // 创建共享内存域
try {
// 分配内存段
MemorySegment segment = arena.allocate(100); // 分配 100 字节
// 使用内存段
segment.set(ValueLayout.JAVA_INT, 0, 42); // 写入数据
// 可以检查内存段是否有效
if (segment.scope().isAlive()) { // 检查内存段是否还活着
System.out.println("内存段有效"); // 输出信息
}
// Arena 关闭后,所有关联的内存段都会失效
} finally {
arena.close(); // 关闭 Arena,释放所有内存
// 关闭后,所有关联的内存段都会失效,访问会抛出异常
}
}
关键是理解内存段的生命周期:它由 Arena 管理,Arena 关闭时,所有关联的内存段都会失效。
实际应用场景
场景1:调用系统库函数
最常见的场景就是调用系统库函数,比如调用 POSIX 的 getpid 函数:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.MethodHandle; // 导入方法句柄类
// 调用系统库函数获取进程 ID
public long getProcessId() throws Throwable {
Linker linker = Linker.nativeLinker(); // 创建链接器
SymbolLookup stdlib = linker.defaultLookup(); // 获取标准库查找器
// 查找 getpid 函数(POSIX 系统)
MemorySegment getpidFunc = stdlib.find("getpid") // 查找函数
.orElseThrow(() -> new RuntimeException("找不到 getpid 函数")); // 如果找不到就抛异常
// 定义函数签名:pid_t getpid(void)
// 返回值是 long(进程 ID),无参数
FunctionDescriptor getpidDesc = FunctionDescriptor.of(ValueLayout.JAVA_LONG); // 定义函数描述符
// 创建方法句柄
MethodHandle getpid = linker.downcallHandle(getpidFunc, getpidDesc); // 创建下行调用句柄
// 调用函数(不需要 Arena,因为没有分配内存)
long pid = (long) getpid.invoke(); // 调用函数,获取进程 ID
return pid; // 返回进程 ID
}
这个场景下,调用系统库函数很简单,不需要分配内存,直接调用就行。
场景2:处理原生内存分配
有时候需要调用原生的 malloc 和 free 函数来管理内存:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.MethodHandle; // 导入方法句柄类
// 使用原生 malloc 和 free 管理内存
public void useNativeMalloc() throws Throwable {
Linker linker = Linker.nativeLinker(); // 创建链接器
SymbolLookup stdlib = linker.defaultLookup(); // 获取标准库查找器
// 查找 malloc 函数:void* malloc(size_t size)
MethodHandle malloc = linker.downcallHandle(
stdlib.findOrThrow("malloc"), // 查找 malloc 函数
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG) // 返回值是地址,参数是大小
);
// 查找 free 函数:void free(void* ptr)
MethodHandle free = linker.downcallHandle(
stdlib.findOrThrow("free"), // 查找 free 函数
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) // 无返回值,参数是地址
);
try (Arena arena = Arena.ofConfined()) { // 创建内存域
// 使用 malloc 分配 100 字节
MemorySegment segment = (MemorySegment) malloc.invokeExact(100L); // 调用 malloc,分配内存
// 重新解释内存段,并附加清理操作
// 当 Arena 关闭时,会自动调用 free
segment = segment.reinterpret(100, arena, s -> { // 重新解释内存段
try {
free.invokeExact(s); // 调用 free 释放内存
} catch (Throwable e) {
throw new RuntimeException(e); // 转换异常
}
});
// 使用分配的内存
segment.set(ValueLayout.JAVA_INT, 0, 42); // 写入数据
int value = segment.get(ValueLayout.JAVA_INT, 0); // 读取数据
System.out.println("值: " + value); // 输出:值: 42
}
// Arena 关闭时,会自动调用 free,释放内存
}
这个场景下,使用原生的 malloc 分配内存,然后用 Arena 管理生命周期,确保内存正确释放。
场景3:处理错误码
调用原生函数时,可能需要检查错误码,比如 POSIX 的 errno:
import java.lang.foreign.*; // 导入 FFM API 包
import java.lang.invoke.MethodHandle; // 导入方法句柄类
// 处理原生函数的错误码
public void handleNativeErrors() throws Throwable {
Linker linker = Linker.nativeLinker(); // 创建链接器
SymbolLookup stdlib = linker.defaultLookup(); // 获取标准库查找器
// 查找 errno 变量(POSIX 系统)
MemorySegment errno = stdlib.find("errno") // 查找 errno 变量
.orElseThrow(() -> new RuntimeException("找不到 errno")); // 如果找不到就抛异常
// 假设调用一个可能失败的原生函数
// 这里只是示例,实际使用时需要根据具体函数调整
try (Arena arena = Arena.ofConfined()) { // 创建内存域
// 调用原生函数
// ...
// 检查 errno(需要知道 errno 的内存布局)
// 注意:errno 的实际布局取决于系统,这里只是示例
int errorCode = errno.get(ValueLayout.JAVA_INT, 0); // 读取错误码
if (errorCode != 0) { // 如果有错误
System.err.println("原生函数调用失败,错误码: " + errorCode); // 输出错误信息
}
}
}
这个场景下,需要知道如何访问 errno 变量来检查错误。注意 errno 的实际布局取决于系统。
使用 jextract 工具生成绑定
手动写 FFM API 代码虽然可行,但对于复杂的原生库,工作量很大。Java 22 提供了 jextract 工具,可以自动从 C 头文件生成 Java 绑定代码。
安装 jextract
jextract 是独立工具,需要单独下载:
# 从 Project Jextract 下载预编译版本
# 或者从源码编译
git clone https://github.com/openjdk/jextract.git
cd jextract
./gradlew build
使用 jextract 生成绑定
假设有一个 C 头文件 mylib.h:
// mylib.h
int add(int a, int b);
void process_data(char* data, int length);
可以用 jextract 生成 Java 绑定:
# 生成 Java 绑定
jextract -l :/path/to/libmylib.so \
--output gensrc \
-I /usr/include \
-t com.example.mylib \
/path/to/mylib.h
这会生成 Java 类,可以直接使用:
import com.example.mylib.mylib_h.*; // 导入生成的绑定类
// 使用生成的绑定
public void useGeneratedBindings() {
// 直接调用原生函数,不需要手动创建 MethodHandle
int result = add(10, 20); // 调用 add 函数
System.out.println("结果: " + result); // 输出:结果: 30
try (Arena arena = Arena.ofConfined()) { // 创建内存域
MemorySegment data = arena.allocateFrom("test data"); // 分配字符串
process_data(data, data.byteSize()); // 调用 process_data 函数
}
}
使用 jextract 生成的绑定,代码更简洁,不需要手动创建 MethodHandle 和 FunctionDescriptor。
最佳实践
使用 FFM API 时,有几个最佳实践需要注意:
1. 总是使用 try-with-resources 管理 Arena
Arena 实现了 AutoCloseable 接口,必须使用 try-with-resources 来确保内存正确释放:
// ✅ 正确:使用 try-with-resources
try (Arena arena = Arena.ofConfined()) { // 使用 try-with-resources
MemorySegment segment = arena.allocate(100); // 分配内存
// 使用内存段
}
// Arena 自动关闭,内存自动释放
// ❌ 错误:手动管理资源
Arena arena = Arena.ofConfined(); // 手动创建
MemorySegment segment = arena.allocate(100); // 分配内存
// 容易忘记关闭,导致内存泄漏
2. 正确选择 Arena 类型
根据使用场景选择合适的 Arena:
// 单线程场景:使用 Confined Arena
try (Arena arena = Arena.ofConfined()) { // 性能最好
// 使用内存段
}
// 多线程场景:使用 Shared Arena
Arena arena = Arena.ofShared(); // 可以在多线程使用
try {
// 使用内存段
} finally {
arena.close(); // 手动关闭
}
3. 注意内存对齐
某些平台对内存对齐有要求,需要注意:
// 使用 MemoryLayout 定义结构体时,会自动处理对齐
StructLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"), // 自动对齐
ValueLayout.JAVA_INT.withName("y") // 自动对齐
);
4. 处理异常
调用原生函数时,需要正确处理异常:
try {
// 调用原生函数
long result = (long) methodHandle.invoke(args); // 调用函数
} catch (Throwable e) { // 捕获异常
// 处理异常
System.err.println("调用原生函数失败: " + e.getMessage()); // 输出错误信息
}
总结
外部函数和内存 API(JEP 454)是 Java 22 引入的一个重要特性,它提供了一种更安全、更简单、更高效的方式来调用原生函数和操作原生内存。核心优势包括:
- 纯 Java 实现:不需要写 C 代码,完全用 Java 就能调用原生函数
- 类型安全:编译时就能检查类型错误,减少运行时错误
- 内存安全:Arena 自动管理内存生命周期,避免内存泄漏
- 性能更好:比 JNI 性能更好,开销更小
- 工具支持:
jextract工具可以自动生成绑定代码
虽然 FFM API 还在预览阶段,但它的设计理念和 API 已经相当成熟了。如果你需要调用原生库、操作原生内存,或者需要高性能的内存操作,强烈建议试试 FFM API,它能让你的代码更安全、更简单。
兄弟们,今天就聊到这里。FFM API 这玩意儿确实能简化原生函数调用,但也要注意内存管理和异常处理。有啥问题欢迎留言讨论,鹏磊会继续分享 Java 22 的其他新特性。