引言
最近终于有时间写点东西。本来想着在去年Java25发布的时候写篇关于ZGC和G1性能对比以及FFM API的实践的文章,但事情有点多,一直没空写。
Foreign Function & Memory API 是 Project Panama 的重要内容,在 JEP-454 正式引入。
可以不再使用 JNI,只需要纯Java代码就能实现 cinterop,突破 ByteBuffer 的 INT.MAX_VALUE上限。
环境
MacOS 26.3、Java 25、Kotlin 2.3.10、IDEA 2025.3.2
JVM参数:-Xmx16M
Memory API
通过引入 Arena,来管理直接内存的生命周期,在结束后调用 close() 进行释放,支持 try-with-resource
引入 MemorySegment,以提供 ByteBuffer 及 Unsafe 之外的方式来使用堆外内存,提供四种使用方式:
-
Arena.global()全局作用域,可以被任意线程访问,手动close报错
-
Arena.ofAuto()堆内,可以被任意线程访问,由GC管理,手动close报错
-
Arena.ofShared()堆外,可以被任意线程访问,手动管理
-
Arena.ofConfined()堆外,只能由创建者线程访问,手动管理
实验环境设计:提供各一个1G和2G的文件,通过堆外内存一次性将文件读入Xmx16M的JVM中,再读取其中的内容
1G文件
Arena.ofConfined().use { arena ->
val memorySegment = arena.allocate(file.fileSize())
// 因为分配的大小小于Int.MAX_VALUE,这里使用asByteBuffer方法
// 所有变化都会映射到原memorySegment上
val byteBuffer = memorySegment.asByteBuffer()
var point = 0L
val bufferSize = 1024 * 1024
val buffer = ByteArray(1024 * 1024)
while (point < file.fileSize()) {
val read = fileIS.readNBytes(buffer, 0, bufferSize)
if (read <= 0) break
byteBuffer.position(point.toInt()).put(buffer, 0, read)
point += read
}
println("Read file into memory segment: ${point / 1024 / 1024.0} MB")
println("===== last 5 lines =====")
var cursor = 0L
val totalLineCnt = 100000000
repeat(totalLineCnt) { i ->
readLine(memorySegment, cursor).let {
cursor = it.first + 1
if (i >= (totalLineCnt - 5)) println(it.second)
}
}
}
2G文件
file.inputStream().use { fileIS ->
Arena.ofConfined().use { arena ->
val memorySegment = arena.allocate(file.fileSize())
var point = 0L
val bufferSize = 1024 * 1024
val buffer = ByteArray(bufferSize)
while (point < file.fileSize()) {
val read = fileIS.readNBytes(buffer, 0, bufferSize)
if (read <= 0) break
// 手动映射
putBytesIntoMemorySegment(
memSegment = memorySegment,
offset = point,
bytes = buffer,
effactiveByteSize = read,
)
point += read
}
println("Read file into memory segment: ${point / 1024 / 1024.0} MB")
println("===== last 5 lines =====")
var cursor = 0L
val totalLineCnt = 200000000
repeat(totalLineCnt) { i ->
readLine(memorySegment, cursor).let {
cursor = it.first + 1
if (i >= (totalLineCnt - 5)) println(it.second)
}
}
}
}
通过 slice 获取原Segment的视图,实现零拷贝,所有对该slice的操作均会映射到原Segment上
val last5LineByteSlice = memorySegment.asSlice(point - 5 * 11)
cursor = 0L
println("===== last 5 lines (slice) =====")
repeat(5) {
readLine(last5LineByteSlice, cursor).let {
cursor = it.first + 1
println(it.second)
}
}
输出:
Xmx: 16.0 MB
File Size: 2098.0830078125 MB
Read file into memory segment: 2098.0830078125 MB
===== last 5 lines (slice) =====
0199999996
0199999997
0199999998
0199999999
0200000000
Foreign Function API
通过 jextract 实现根据头文件自动的生成绑定/描述文件,避免以往通过 JNI 进行 cinterop 时手动编写描述文件,可以通过pure Java代码完成整个cinterop
// 拿到linker对象,是一个与原生平台关联的 ABI 链接器
val linker = Linker.nativeLinker()
// 查找strlen在标准库里的符号地址
val stdlib = linker.defaultLookup()
val methodAddr = stdlib.find("strlen").orElseThrow()
// 手动定义函数描述
val strlenFuncDesc =
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
)
val functionCall = linker.downcallHandle(methodAddr, strlenFuncDesc)
val (_, lastLineStr) = readLine(memorySegment, point - 11)
Arena.ofConfined().use { arena2 ->
// Converts a Java string into a null-terminated C string using the UTF-8 charset
val lastLineStrMemSeg = arena2.allocateFrom(lastLineStr)
val result = functionCall.invoke(lastLineStrMemSeg) as Long
println("strlen of last line: $result")
}
输出:
0200000000
strlen of last line: 10
总结
通过 Arena 管理生命周期,使得直接内存的释放具备结构化语义(try-with-resources),相比早期的 Unsafe 或 DirectByteBuffer 更加安全、清晰。
尤其是在大内存场景下,MemorySegment 解决了 ByteBuffer 受 Int.MAX_VALUE 限制的问题,使得 2GB 以上内存访问成为可能。
可以实现纯 Java 层面的 native 调用,大幅降低跨语言交互的复杂度。
对于简单函数调用场景(如 libc、压缩库、图像库等),FFM 已具备替代 JNI 的能力。
参考:
- Panama
- https://zhuanlan.zhihu.com/p/2001957915553207526