引言

最近终于有时间写点东西。本来想着在去年Java25发布的时候写篇关于ZGC和G1性能对比以及FFM API的实践的文章,但事情有点多,一直没空写。

Foreign Function & Memory APIProject Panama 的重要内容,在 JEP-454 正式引入。

可以不再使用 JNI,只需要纯Java代码就能实现 cinterop,突破 ByteBufferINT.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,以提供 ByteBufferUnsafe 之外的方式来使用堆外内存,提供四种使用方式:

  • 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),相比早期的 UnsafeDirectByteBuffer 更加安全、清晰。

尤其是在大内存场景下,MemorySegment 解决了 ByteBufferInt.MAX_VALUE 限制的问题,使得 2GB 以上内存访问成为可能。

可以实现纯 Java 层面的 native 调用,大幅降低跨语言交互的复杂度。

对于简单函数调用场景(如 libc、压缩库、图像库等),FFM 已具备替代 JNI 的能力。


代码仓库


参考:

  1. Panama
  2. https://zhuanlan.zhihu.com/p/2001957915553207526

ねぇ,あなたは何色になりたい