JDK JDK 学习手册8 → 27 · JDK 专用学习站
DEEPDIVE · FFM · JEP 442 / 454

FFM:用 Arena 和 MemorySegment 取代 JNI

JDK 21 · 深钻主题 08 / 08 · 预计 55 分钟
进阶四步深钻JEP 442JEP 454需要 --enable-preview
一句话Foreign Function & Memory API 让 Java 直接调用本地函数、管理堆外内存,并用 Arena 把「谁负责释放」变成可验证的生命周期约束。JDK 21 是第三轮预览,JDK 22 转正。
本质:FFM 的核心不是「能调 C」,而是把本地内存的生命周期和访问边界变成 Java 可检查的约束:Arena 决定何时释放,MemorySegment 决定能访问多大范围。

学完你要能自己回答

  1. FFM 相比 JNI 和 sun.misc.Unsafe 分别解决了什么问题?
  2. Arena.ofConfined / ofShared / ofAuto / global 的寿命与线程约束分别是什么?
  3. downcall 与 upcall 的方向分别是什么?FunctionDescriptor 如何描述 C 函数?
  4. MemorySegment 的边界检查能防住什么,又防不住什么?
  5. JDK 21 与 JDK 22 的 FFM API 有哪些命名差异?
01 为什么还需要一套新的本地调用 API02 五个对象:Arena、Segment、Layout、Linker、Lookup03 三层安全:边界、寿命、线程

为什么还需要一套新的本地调用 API01

JNI 能调本地库,但需要写 C 头文件、生成胶水代码、处理引用和异常,构建链复杂;sun.misc.Unsafe 能直接读写内存,却没有边界检查和生命周期管理,一次越界就是 JVM 崩溃。FFM 试图把「调本地代码」变成 Java 语言内可描述、可检查的操作。

维度JNIsun.misc.UnsafeFFM
胶水代码需要 C 侧胶水不需要不需要
内存边界自行保证无检查MemorySegment 边界检查
生命周期手动管理引用手动 freeArena 作用域管理
线程约束自行保证自行保证Confined / Shared 显式建模
调用开销较高(JNI 边界)—接近 JNI,通常优于反射
标准状态长期标准内部 API,不保证JDK 22 正式

FFM 不是「让 Java 变成 C」。它仍然需要你知道 C 函数的签名、平台相关的类型宽度和内存布局;它只是把最容易出错的边界与寿命交给了运行时检查。

FFM 适合替换 JNI 胶水、调用系统库、处理大块堆外数据。普通文件读写、网络、JSON 解析不需要 FFM;标准库或成熟库通常更安全。

五个对象:Arena、Segment、Layout、Linker、Lookup02

对象角色典型 API
Arena内存分配器 + 生命周期ofConfined / ofShared / ofAuto / global / close
MemorySegment带边界的本地内存视图allocate / get / set / asSlice / byteSize
MemoryLayout描述 C 结构体 / 数组的布局structLayout / sequenceLayout / ValueLayout
Linker把 Java 方法句柄接到本地函数nativeLinker / downcallHandle / upcallStub
SymbolLookup按名字查找本地符号defaultLookup / libraryLookup

downcall 是 Java → native:用 FunctionDescriptor 描述参数和返回值,拿到 MethodHandle,再用 invokeExact 调用。upcall 是 native → Java:把 Java 方法句柄包装成函数指针,交给本地代码回调。

// downcall 的典型形状
MethodHandle strlen = linker.downcallHandle(
        lookup.find("strlen").orElseThrow(),
        FunctionDescriptor.of(JAVA_LONG, ADDRESS));

// upcall 的典型形状
MemorySegment stub = linker.upcallStub(
        javaMethodHandle,
        FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT),
        arena);

FunctionDescriptor 必须和 C 签名一致。C 的 int、long、size_t 在不同平台上宽度不同;跨平台代码应使用 ValueLayout 中对应的 C 类型,而不是想当然地用 JAVA_LONG。

三层安全:边界、寿命、线程03

  • 空间安全:每次 get/set 都带布局和偏移,越界会抛 IndexOutOfBoundsException,而不是读写到相邻内存。
  • 时间安全:Segment 归属于某个 Arena;Arena 关闭后继续访问会抛 IllegalStateException: Already closed。
  • 线程安全:ofConfined 的 Segment 只能由创建它的线程访问,其他线程访问会抛 WrongThreadException;需要跨线程就用 ofShared。
// 本机 JDK 21 验证
confined other thread -> java.lang.WrongThreadException:
  Attempted access outside owning thread

closed access -> java.lang.IllegalStateException: Already closed

它防不住的事情同样重要:本地代码内部越界、写坏指针、释放后继续使用、错误的 C 类型宽度、reinterpret 强行扩大边界。FFM 把 Java 侧的访问变得可检查,但无法验证 native 库内部的行为。

MemorySegment.reinterpret 是逃生舱:它会扩大或改变 Segment 的可见范围,绕过边界检查。除非 native API 明确返回「指针 + 长度」,否则不要使用。
01 Arena:把释放责任变成作用域02 MemorySegment:带边界的地址03 downcall:从 Java 调到 C04 upcall:从 C 调回 Java

Arena:把释放责任变成作用域01

Arena生命周期线程约束适用场景
ofConfined()手动 close()只能由创建线程访问单线程短生命周期调用
ofShared()手动 close()可被多个线程访问跨线程共享的本地内存
ofAuto()GC 回收,不能 close无想少管释放、可接受非确定性回收
global()永不释放无进程级长期常量,谨慎使用

绝大多数调用应该用 try (Arena arena = Arena.ofConfined()) { ... }。Arena 关闭时统一释放其中分配的 Segment;你不需要、也不能单独 free 某个 Segment。

try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(16);
    segment.set(JAVA_INT, 0, 42);
    System.out.println(segment.get(JAVA_INT, 0)); // 42
}
// 离开 try 后 arena 已关闭,segment 不能再用

ofConfined 的「confined」不是建议,而是强制:在另一个线程访问它的 Segment 会抛 WrongThreadException。这能尽早暴露「把 Segment 逃逸到线程池」的错误。

不要把 ofConfined 分配的 Segment 存进缓存或返回给调用方;它的寿命不会超过 Arena。需要跨线程或跨方法长期使用,应使用 ofShared 或明确的生命周期管理。

MemorySegment:带边界的地址02

MemorySegment 由「地址 + 字节长度 + 所属 scope」组成。所有读写都通过 ValueLayout 指定类型和偏移,运行时检查偏移是否在范围内。

try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(2 * Integer.BYTES);
    segment.set(JAVA_INT, 0, 7);
    segment.set(JAVA_INT, Integer.BYTES, 9);

    System.out.println(segment.get(JAVA_INT, 0));          // 7
    System.out.println(segment.get(JAVA_INT, Integer.BYTES)); // 9
    System.out.println(segment.byteSize());                // 8

    MemorySegment second = segment.asSlice(Integer.BYTES, Integer.BYTES);
    System.out.println(second.get(JAVA_INT, 0));           // 9
}

JDK 21 用 allocateUtf8String / getUtf8String 处理 C 字符串;JDK 22 转正后改名为 allocateFrom / getString。这两个名字在 21 与 22+ 之间不能混用。

操作JDK 21 预览JDK 22+ 正式
分配 UTF-8 字符串arena.allocateUtf8String(s)arena.allocateFrom(s)
读取 UTF-8 字符串segment.getUtf8String(0)segment.getString(0)
分配结构体arena.allocate(layout)arena.allocate(layout)
堆内数组视图MemorySegment.ofArray(array)MemorySegment.ofArray(array)
Segment 的 byteSize 是字节数,不是元素个数。UTF-8 字符串的字节数还包含结尾的 NUL,所以 "Hello, JDK 21!"(14 字节)分配出来是 15 字节。

downcall:从 Java 调到 C03

以 C 标准库的 strlen(const char*) 为例:参数是指针(ADDRESS),返回值是 size_t。在 64 位平台上用 JAVA_LONG 接住返回值。

Linker linker = Linker.nativeLinker();
SymbolLookup lookup = linker.defaultLookup();

MethodHandle strlen = linker.downcallHandle(
        lookup.find("strlen").orElseThrow(),
        FunctionDescriptor.of(JAVA_LONG, ADDRESS));

try (Arena arena = Arena.ofConfined()) {
    MemorySegment text = arena.allocateUtf8String("Hello, JDK 21!");
    long length = (long) strlen.invokeExact(text);
    System.out.println("strlen=" + length); // 14
    System.out.println("size=" + text.byteSize()); // 15
}
javac --enable-preview --release 21 FfmStrlenDemo.java
java --enable-preview --enable-native-access=ALL-UNNAMED FfmStrlenDemo
strlen=14
size=15
  • lookup.find("strlen") 在默认查找范围内找符号;找不到时返回空 Optional。
  • FunctionDescriptor.of(returnLayout, paramLayouts...) 的顺序必须与 C 签名一致。
  • invokeExact 的类型必须与方法句柄完全匹配,否则抛 WrongMethodTypeException。
  • 返回值是 size_t;在 64 位平台用 JAVA_LONG,跨平台应使用与平台宽度对应的布局。
不给 --enable-native-access=ALL-UNNAMED 也能运行,但 JDK 21 会打印 restricted method 警告。生产启动参数应显式声明 native access,而不是依赖警告被忽略。

upcall:从 C 调回 Java04

upcall 把 Java 方法句柄变成 native 可调用的函数指针。下面的例子不依赖外部库:先创建 upcall stub,再把 stub 当作函数指针,通过 downcall 调回 Java,验证参数和返回值都正确。

Linker linker = Linker.nativeLinker();
MethodHandle add = MethodHandles.lookup().findStatic(
        FfmUpcallDemo.class, "add",
        MethodType.methodType(int.class, int.class, int.class));

FunctionDescriptor descriptor =
        FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment stub = linker.upcallStub(add, descriptor, arena);
    MethodHandle caller = linker.downcallHandle(stub, descriptor);
    int result = (int) caller.invokeExact(20, 22);
    System.out.println("upcall result=" + result); // 42
}
upcall result=42
  • upcall stub 的生命周期由传入的 Arena 管理;Arena 关闭后再调用 stub 是未定义行为。
  • 回调方法的参数/返回值必须与 FunctionDescriptor 匹配;C 侧不会做 Java 类型检查。
  • 不要让异常越过 upcall 边界:在 Java 回调内部捕获并转换为错误码或返回值。
  • upcall 适合事件回调、比较器、日志钩子等场景;高频回调要评估线程切换与异常处理成本。
01 可运行示例:调用 strlen02 可运行示例:upcall stub03 版本迁移与选型04 常见坑

可运行示例:调用 strlen01

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import static java.lang.foreign.ValueLayout.*;

public class FfmStrlenDemo {
    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        SymbolLookup lookup = linker.defaultLookup();
        MethodHandle strlen = linker.downcallHandle(
                lookup.find("strlen").orElseThrow(),
                FunctionDescriptor.of(JAVA_LONG, ADDRESS));

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment text = arena.allocateUtf8String("Hello, JDK 21!");
            long length = (long) strlen.invokeExact(text);
            System.out.println("strlen=" + length);
            System.out.println("size=" + text.byteSize());
        }
    }
}
javac --enable-preview --release 21 FfmStrlenDemo.java
java --enable-preview --enable-native-access=ALL-UNNAMED FfmStrlenDemo
strlen=14
size=15

defaultLookup() 在 Windows 与 Linux/macOS 上都能找到 C 运行时的 strlen;如果换成第三方库,需要用 SymbolLookup.libraryLookup("mylib", arena) 加载动态库。

可运行示例:upcall stub02

import java.lang.foreign.*;
import java.lang.invoke.*;
import static java.lang.foreign.ValueLayout.*;

public class FfmUpcallDemo {
    static int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        MethodHandle add = MethodHandles.lookup().findStatic(
                FfmUpcallDemo.class, "add",
                MethodType.methodType(int.class, int.class, int.class));

        FunctionDescriptor descriptor =
                FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT);

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment stub = linker.upcallStub(add, descriptor, arena);
            MethodHandle caller = linker.downcallHandle(stub, descriptor);
            int result = (int) caller.invokeExact(20, 22);
            System.out.println("upcall result=" + result);
        }
    }
}
upcall result=42

这个例子的价值在于把 upcall 的整条链路都放在 Java 里,不需要编译 C 库。真实场景中,stub 会作为回调参数传给 qsort、事件循环或第三方库。

版本迁移与选型03

事项JDK 21JDK 22+
状态第三轮预览(JEP 442)正式(JEP 454)
编译--enable-preview --release 21--release 22
运行--enable-preview不需要 preview
native access--enable-native-access=ALL-UNNAMED同样建议显式声明
字符串分配allocateUtf8StringallocateFrom
字符串读取getUtf8StringgetString
# JDK 21
javac --enable-preview --release 21 FfmStrlenDemo.java
java --enable-preview --enable-native-access=ALL-UNNAMED FfmStrlenDemo

# JDK 22+(API 改名后)
javac --release 22 FfmStrlenDemo.java
java --enable-native-access=ALL-UNNAMED FfmStrlenDemo

什么时候值得用 FFM

  • 已有 C/C++ 库,且不想再维护一层 JNI 胶水代码。
  • 需要大块堆外内存、内存映射或与 native 库共享缓冲区。
  • 需要回调 native 库(upcall),例如排序比较器、事件监听。
  • 对启动/调用开销有要求,且标准库没有合适抽象。

什么时候不该用

  • 普通文件、网络、序列化:优先 Files、HttpClient、成熟库。
  • 只想「绕过 GC」:先确认堆外内存的分配、释放和排查成本是否值得。
  • 跨平台库的 C 类型宽度没有核对:错误布局比性能问题更常见。
  • 团队没有 native 调试能力:崩溃栈通常不会像 Java 异常那样友好。
FFM 的正式化不意味着 JNI 立刻消失。已有 JNI 库可以继续用;新项目在需要 native 互操作时,优先评估 FFM 是否能减少胶水代码和维护成本。

常见坑04

  • Segment 逃逸出 Arena:把 ofConfined 的 Segment 返回或缓存,Arena 关闭后访问会 Already closed。
  • 跨线程使用 confined Segment:抛 WrongThreadException;跨线程用 ofShared。
  • 忘记 native access 参数:FFM 能运行但会警告;生产启动脚本应显式声明。
  • C 类型宽度假设错误:Windows 的 C long 是 32 位,不要一律用 JAVA_LONG。
  • reinterpret 扩大边界:绕过检查,可能直接读坏内存。
  • upcall 中抛异常:异常不能安全跨越 native 边界,回调内部要自行处理。
  • 把普通 IO 迁到 FFM:复杂度上升,收益通常为负。
FFM 的调试线索通常来自三类异常:IllegalStateException: Already closed(寿命)、WrongThreadException(线程)、IndexOutOfBoundsException(边界)。先按这三类定位,再怀疑 native 库本身。
01 先猜再看02 费曼自检:生命周期与边界

先猜再看01

先猜再看Arena.ofConfined() 分配的 Segment 被另一个线程访问,会发生什么?
抛 WrongThreadException(Attempted access outside owning thread)。Confined 的约束是强制的,不是文档建议。
先猜再看downcall 与 upcall 的方向分别是什么?
downcall 是 Java 调 native;upcall 是把 Java 方法包装成函数指针,让 native 调回 Java。
先猜再看Arena 关闭后再访问其中的 Segment 会怎样?
抛 IllegalStateException: Already closed。Segment 的 scope 已经失效,不能再读写。

费曼自检:生命周期与边界02

01. FFM 相比 JNI 最大的工程收益是什么?
不需要编写和编译 C 胶水代码;用 Java 的 MethodHandle/FunctionDescriptor 描述调用,并把内存生命周期交给 Arena 管理。
02. Arena.ofConfined、ofShared、ofAuto、global 各适合什么场景?
Confined 适合单线程短生命周期;Shared 适合跨线程共享;Auto 由 GC 回收、不保证时点;global 永不释放,适合进程级常量。
03. MemorySegment 的边界检查能防住什么?
能防住 Java 侧按布局读写时的越界访问;防不住 native 库内部越界、错误的 C 类型宽度、reinterpret 绕过检查以及 use-after-free。
04. 为什么 JDK 21 的 FFM 代码不能直接拿到 JDK 22 编译?
JDK 21 是预览 API,且 allocateUtf8String/getUtf8String 在 JDK 22 正式版改名为 allocateFrom/getString;需要按目标版本改代码和编译参数。
05. upcall stub 的寿命由什么决定?
由创建它时传入的 Arena 决定;Arena 关闭后 stub 不能再被调用。回调方法必须与 FunctionDescriptor 匹配。
06. 什么情况下不应该用 FFM?
普通文件/网络 IO、序列化、已有成熟标准库或库能解决的场景;FFM 的复杂度和 native 调试成本通常不值得。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。