1. 概述

Java 对象默认分配在堆内存中,这在某些场景下会带来问题:内存利用率低、性能瓶颈、GC 压力大。而使用 native 内存(即堆外内存)往往更高效,但传统方式操作起来既复杂又容易出错。

Java 14 引入了 Foreign Memory Access API,旨在更安全高效地访问堆外内存。此后经过多个版本演进,最终在 Java 22 中与 Foreign Linker API 合并,统称为 Foreign Function & Memory API(FFM API)

本文将带你深入理解这一现代 Java 原生互操作能力的核心机制和使用方式。


2. 为什么需要这个 API?

高效内存管理一直是系统级编程的难点,核心在于对内存布局、寻址机制理解不足。例如,一个设计不当的缓存可能导致频繁 Full GC,严重拖慢应用性能。

在 FFM API 出现前,Java 主要通过两种方式操作堆外内存:ByteBuffersun.misc.Unsafe。我们来快速回顾下它们的优劣。

2.1. ByteBuffer API

ByteBuffer.allocateDirect() 可以创建堆外字节缓冲区,直接映射到 native 内存。

✅ 优点:

  • 标准 API,无需额外依赖
  • 可绕过 JVM 堆,减少 GC 压力

❌ 缺点:

  • 单个 Buffer 最大不超过 2GB(受限于 int 索引)
  • 依赖 GC 回收,存在内存泄漏风险(如 Direct Buffer 泄漏导致 OutOfMemoryError
  • 操作粒度粗,缺乏结构化访问支持

⚠️ 踩坑提示:频繁创建 DirectByteBuffer 容易触发 MetaspaceNative Memory 耗尽,尤其是在高并发场景下。

2.2. Unsafe API

sun.misc.Unsafe 提供了近乎 C 语言级别的内存操作能力。

✅ 优点:

  • 性能极高,直接操作内存地址
  • 支持任意内存读写、CAS、内存映射等

❌ 缺点:

  • 非公开 API,随时可能被移除
  • 极其危险,错误使用可导致 JVM 崩溃(如空指针解引用、越界访问)

这玩意儿就像一把没有保险的枪,威力大但容易走火。

2.3. JNI API

Java 从 1.1 就支持调用 native 代码(JNI),但开发体验极差:

❌ 主要痛点:

  • 需要编写 C/C++ 代码 + 编译工具链
  • 跨平台编译复杂(Windows/Linux/macOS 各不同)
  • 数据类型转换繁琐,易出错
  • 调用约定(calling convention)处理麻烦

2.4. 新 API 的必要性

总结一下,旧方案陷入两难:

方案 安全性 效率 易用性
ByteBuffer ✅ 高 ⚠️ 中 ✅ 高
Unsafe ❌ 低 ✅ 高 ❌ 低
JNI ✅ 高 ✅ 高 ❌ 低

FFM API 的目标就是:安全 + 高效 + 易用三位一体。


3. Foreign Function & Memory API 核心组件

FFM API 提供了一套标准化、类型安全的接口,用于访问堆内外内存并调用 native 函数。核心组件如下:

  • Arena:管理 native 内存生命周期
  • MemorySegment:表示一段连续内存区域(堆内或堆外)
  • MemoryLayout:描述内存结构布局(类似 C 的 struct)
  • FunctionDescriptor:定义 native 函数签名
  • Linker:连接 Java 与 native 函数
  • SymbolLookup:查找动态库中的函数符号

下面我们逐个深入。


4. 原生内存分配

4.1. Arena:内存生命周期管理者

Arena 是 FFM API 的核心,负责内存段(MemorySegment)的分配与自动释放。Java 22 提供了多种类型的 Arena,适用于不同场景:

// 全局 Arena:生命周期无限,不能手动关闭
Arena globalArena = Arena.global();
MemorySegment segment = globalArena.allocate(100);
// 自动 Arena:由 GC 管理,当引用不可达时自动释放
Arena arena = Arena.ofAuto();
MemorySegment segment = arena.allocate(100);
// 线程独占 Arena:仅创建线程可访问,适合单线程高性能场景
Arena arena = Arena.ofConfined();
MemorySegment segment = arena.allocate(100);
// 共享 Arena:允许多线程访问,需注意同步
Arena arena = Arena.ofShared();
MemorySegment segment = arena.allocate(100);

⚠️ 建议:大多数场景推荐使用 ofAuto(),兼顾安全与便利。


4.2. MemorySegment:内存段抽象

MemorySegment 表示一段连续的内存区域,可以是堆内或堆外。获取方式多样:

创建 native 内存段(堆外)

Arena arena = Arena.ofAuto();
MemorySegment memorySegment = arena.allocate(200); // 200 字节堆外内存

从 Java 数组创建(堆内)

MemorySegment memorySegment = MemorySegment.ofArray(new long[100]);

从 Direct ByteBuffer 创建

MemorySegment memorySegment = MemorySegment.ofBuffer(ByteBuffer.allocateDirect(200));

从内存映射文件创建

Arena arena = Arena.ofConfined();
RandomAccessFile file = new RandomAccessFile("/tmp/memory.txt", "rw");
FileChannel fc = file.getChannel();
MemorySegment memorySegment = fc.map(READ_WRITE, 0, 200, arena);

✅ 安全保障:

  • 空间边界:访问不能越界
  • 时间边界:Arena 关闭后无法访问 两者结合确保 JVM 安全,杜绝野指针。

4.3. 内存段切片(Slicing)

可以将一个大内存段切分为多个小段,避免频繁分配。

Arena arena = Arena.ofAuto();
MemorySegment memorySegment = arena.allocate(12); // 12 字节

// 切成 3 个 4 字节段
MemorySegment segment1 = memorySegment.asSlice(0, 4);
MemorySegment segment2 = memorySegment.asSlice(4, 4);
MemorySegment segment3 = memorySegment.asSlice(8, 4);

VarHandle intHandle = ValueLayout.JAVA_INT.varHandle();

intHandle.set(segment1, 0, Integer.MIN_VALUE);
intHandle.set(segment2, 0, 0);
intHandle.set(segment3, 0, Integer.MAX_VALUE);

assertEquals(intHandle.get(segment1, 0), Integer.MIN_VALUE);
assertEquals(intHandle.get(segment2, 0), 0);
assertEquals(intHandle.get(segment3, 0), Integer.MAX_VALUE);

简单粗暴地说:这就像把一块大蛋糕切成三块,每块独立使用但共享底层内存。


5. 使用原生内存:结构化访问

5.1. MemoryLayout:内存布局描述

MemoryLayout 用于定义内存结构,类似于 C 的 struct,但无需定义 Java 类。

int numberOfPoints = 10;
MemoryLayout pointLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("x"), 
    ValueLayout.JAVA_INT.withName("y")
);
SequenceLayout pointsLayout = MemoryLayout.sequenceLayout(numberOfPoints, pointLayout);

上述代码定义了一个包含 10 个点的数组结构,每个点有两个 int 字段:x 和 y。


5.2. ValueLayout:基础类型布局

ValueLayout 表示基本数据类型的内存布局。

ValueLayout intLayout = ValueLayout.JAVA_INT;
ValueLayout charLayout = ValueLayout.JAVA_CHAR;

assertEquals(intLayout.byteSize(), 4); // int 占 4 字节
assertEquals(charLayout.byteSize(), 2); // char 占 2 字节

常见类型:

  • JAVA_INT, JAVA_LONG, JAVA_FLOAT, JAVA_DOUBLE
  • JAVA_CHAR, JAVA_BYTE, ADDRESS(指针)

5.3. SequenceLayout:序列布局

表示重复结构,类似数组。

SequenceLayout sequenceLayout = MemoryLayout.sequenceLayout(10, ValueLayout.JAVA_INT);

等价于 C 的 int arr[10];


5.4. GroupLayout:组合布局

用于构建复杂结构,支持 struct(顺序排列)和 union(重叠排列)。

struct 示例(顺序存储)

GroupLayout groupLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT,
    ValueLayout.JAVA_LONG
);

union 示例(共享起始地址)

GroupLayout groupLayout = MemoryLayout.unionLayout(
    ValueLayout.JAVA_INT,
    ValueLayout.JAVA_LONG
);

union 的大小等于最大成员,所有成员从同一偏移开始。

复杂结构示例

MemoryLayout memoryLayout1 = ValueLayout.JAVA_INT;
MemoryLayout memoryLayout2 = MemoryLayout.structLayout(ValueLayout.JAVA_LONG);
MemoryLayout complexLayout = MemoryLayout.structLayout(
    memoryLayout1,
    MemoryLayout.paddingLayout(4), // 补齐对齐
    memoryLayout2
);

5.5. VarHandle:类型安全的内存访问

VarHandle 是访问 MemorySegment 的关键工具,支持类型安全的读写。

int value = 10;
MemoryLayout pointLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("x"),
    ValueLayout.JAVA_INT.withName("y")
);
VarHandle xHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("x"));

Arena arena = Arena.ofAuto();
MemorySegment segment = arena.allocate(pointLayout);

xHandle.set(segment, 0, value);
int xValue = (int) xHandle.get(segment, 0);

assertEquals(xValue, value);

✅ 优势:相比 UnsafeVarHandle 提供编译期检查和类型安全,避免误操作。


5.6. 使用偏移量访问(带索引的 VarHandle)

结合 SequenceLayout,可用偏移量访问数组元素。

int numberOfPoints = 10;
MemoryLayout pointLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("x"),
    ValueLayout.JAVA_INT.withName("y")
);
SequenceLayout pointsLayout = MemoryLayout.sequenceLayout(numberOfPoints, pointLayout);

VarHandle xHandle = pointsLayout.varHandle(
    MemoryLayout.PathElement.sequenceElement(),
    MemoryLayout.PathElement.groupElement("x")
);

Arena arena = Arena.ofAuto();
MemorySegment segment = arena.allocate(pointsLayout);

// 写入 0~9
for (int i = 0; i < numberOfPoints; i++) {
    xHandle.set(segment, 0, i, i);
}

// 验证
for (int i = 0; i < numberOfPoints; i++) {
    assertEquals(i, (int) xHandle.get(segment, 0, i));
}

内存总大小:10 × (4+4) = 80 字节。VarHandle 自动生成正确偏移。


6. 调用 native 函数

FFM API 让调用 native 函数变得像调用 Java 方法一样简单。

6.1. FunctionDescriptor:函数签名描述

定义 native 函数的参数和返回类型。

FunctionDescriptor fd = FunctionDescriptor.of(
    ValueLayout.JAVA_LONG,    // 返回 long
    ValueLayout.ADDRESS       // 参数为指针
);

ADDRESS 表示内存地址,常用于字符串、结构体指针传参。


6.2. Linker 与 SymbolLookup:链接与符号查找

  • Linker:管理 native 库加载
  • SymbolLookup:查找函数符号
Linker linker = Linker.nativeLinker();
var symbol = linker.defaultLookup().find("strlen").orElseThrow();

defaultLookup() 查找系统默认库(如 glibc)。


6.3. MethodHandle:调用 native 函数

MethodHandle 是 Java 代码调用 native 函数的桥梁。

MethodHandle strlen = linker.downcallHandle(
    symbol,
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);

完整示例:调用 strlen

Linker linker = Linker.nativeLinker();
var symbol = linker.defaultLookup().find("strlen").orElseThrow();

MethodHandle strlen = linker.downcallHandle(
    symbol,
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);

Arena arena = Arena.ofAuto();
MemorySegment str = arena.allocateFrom("Hello"); // 自动加 \0
long len = (long) strlen.invoke(str);

assertEquals(5, len);

✅ 安全高效:无需 JNI,纯 Java 实现 native 调用。


7. 总结

Java 22 的 Foreign Function & Memory API 是一次重大进化:

  • ✅ 替代 UnsafeByteBuffer,提供更安全的堆外内存访问
  • ✅ 统一内存与 native 调用,简化 JNI 复杂性
  • ✅ 类型安全、结构化内存操作,提升开发效率
  • ✅ 基于 Arena 的自动生命周期管理,避免内存泄漏

对于需要高性能、低延迟、或与 native 库交互的场景(如数据库驱动、网络库、AI 推理引擎),FFM API 是未来首选方案。

文中所有示例代码已托管至 GitHub:https://github.com/baeldung/core-java-modules/tree/master/core-java-22


原始标题:Foreign Function and Memory API in Java 22 | Baeldung