Java Foreign Function & Memory API
Call native code and access off-heap memory in modern Java with the Foreign Function and Memory API.
Java Foreign Function & Memory API
The Foreign Function and Memory (FFM) API is Java's modern, safe way to do two things that once demanded the fragile Java Native Interface (JNI): call functions written in C and other native languages, and read and write memory that lives outside the Java heap. It became a final feature in JDK 22 and lives in the java.lang.foreign package.
Before FFM, talking to native code meant hand-written JNI glue, manual byte buffers, and a constant risk of crashing the JVM with a stray pointer. FFM replaces all of that with a small, type-safe API built around three ideas: an Arena controls the lifetime of memory, a MemorySegment is a bounds-checked view into it, and a Linker builds a callable handle to a native function. This chapter walks through each one.
Off-Heap Memory with Arena and MemorySegment
A MemorySegment is a contiguous region of memory with a known size. Unlike a Java array, it can live off the heap, so the garbage collector never moves it and it can be handed straight to native code. You never construct a segment directly — you ask an Arena for one, and the arena owns the segment's lifetime.
When the arena closes, every segment it allocated is freed at once. This makes leaks and use-after-free bugs hard to write: touch a segment after its arena closes and you get an exception, not a crash.
import java.lang.foreign.*;
try (Arena arena = Arena.ofConfined()) {
// Allocate room for four ints, off the Java heap.
MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT, 4);
seg.setAtIndex(ValueLayout.JAVA_INT, 0, 100);
int first = seg.getAtIndex(ValueLayout.JAVA_INT, 0);
System.out.println(first); // 100
} // arena.close() frees the segment hereEvery read and write goes through a ValueLayout, which says exactly how many bytes a value occupies and how it is laid out. That is what keeps each access bounds-checked and type-safe.
Choosing an Arena
Arena is the lifetime manager, and the factory method you pick decides who may touch the memory and when it is released. Choosing the right one is the main safety decision in FFM code.
| Arena | Lifetime | Thread access |
|---|---|---|
Arena.ofConfined() | Until close() | Only the creating thread |
Arena.ofShared() | Until close() | Any thread |
Arena.ofAuto() | Until the GC collects it | Any thread |
Arena.global() | The whole program | Any thread |
Use ofConfined() for the common case: short-lived memory used by one thread and freed deterministically with try-with-resources. Reach for ofShared() only when several threads must read the same segment, and ofAuto() when you cannot easily mark the end of the lifetime.
Describing Layouts
A ValueLayout describes a single primitive value; a MemoryLayout can describe whole structs and arrays. Layouts let you compute offsets and sizes without hard-coding magic numbers, which keeps native struct access readable.
import java.lang.foreign.*;
import static java.lang.foreign.ValueLayout.*;
// A C struct: struct Point { int x; int y; };
MemoryLayout point = MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment p = arena.allocate(point);
var xHandle = point.varHandle(MemoryLayout.PathElement.groupElement("x"));
var yHandle = point.varHandle(MemoryLayout.PathElement.groupElement("y"));
xHandle.set(p, 0L, 3);
yHandle.set(p, 0L, 4);
System.out.println(xHandle.get(p, 0L) + ", " + yHandle.get(p, 0L)); // 3, 4
}The named fields and PathElement accessors mean you describe the struct once and let the API compute byte offsets for you.
Calling Native Functions with Linker
The headline feature of FFM is the downcall: invoking a C function from Java. You get the platform Linker, look up the function's address with a SymbolLookup, describe its signature with a FunctionDescriptor, and receive a MethodHandle you can invoke like any Java method.
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
Linker linker = Linker.nativeLinker();
// strlen lives in the standard C library, found via the default lookup.
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
// size_t strlen(const char *s);
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateUtf8String("hello");
long len = (long) strlen.invoke(cString); // 5
}The FunctionDescriptor maps C types to Java carriers: a C pointer becomes ValueLayout.ADDRESS, a C size_t maps to JAVA_LONG, a C int to JAVA_INT. Get the mapping right and the call is type-safe; get it wrong and you learn at link time, not as a random crash. Because native calls escape the JVM's safety net, FFM is a restricted operation — the module that uses it must be granted access with the --enable-native-access flag.
A Complete, Runnable Example
The java.lang.foreign API is a preview feature before JDK 22, so the program below runs the same two ideas — off-heap memory and native-style string handling — using only the always-on JDK classes that FFM was designed to replace. A direct ByteBuffer is memory allocated outside the Java heap, just like a MemorySegment; reading typed values at byte offsets mirrors a ValueLayout access; and scanning bytes until a zero terminator is exactly what C's strlen does.
What to take from the run:
isDirect = trueconfirms the buffer is allocated off the Java heap — the same property that lets aMemorySegmentbe passed safely to native code without the GC relocating it.- Writing
(i + 1) * 10at each 4-byte offset and reading it back yields10, 20, 30, 40withsum = 100, showing that off-heap memory is real, indexable, typed storage just like aMemorySegment. byteSize = 16is four 4-byte ints — addressing by explicit byte offset is exactly how aValueLayoutcomputes positions in the real FFM API.- The hand-built
cStringends in a zero byte, so the strlen-style scan stops there:strlen of the C string = 16matchesJava String.length() = 16, proving the null terminator marks the end the way C expects. - No buffer is freed by hand — direct buffers are reclaimed when unreachable, mirroring
Arena.ofAuto(), while the real FFMofConfined()arena would free deterministically atclose().
Practice
In the FFM API, what is the role of an Arena?