Java Foreign Function & Memory API
Call native code and access off-heap memory in modern Java with the Foreign Function and 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.
This chapter covers how off-heap memory works in FFM, how an Arena controls its lifetime, how layouts describe native data, and how to call a C function from Java. By the end you should understand when FFM is the right tool and how its pieces fit together.
Why FFM Replaces JNI
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. A single mismatched type or off-by-one offset could corrupt the heap or segfault the whole process — and because the crash happened in native code, you got no Java stack trace.
FFM replaces all of that with a small, type-safe API built around three ideas:
- An
Arenacontrols the lifetime of memory: when it closes, everything it allocated is freed. - A
MemorySegmentis a bounds-checked view into that memory, so out-of-range access throws an exception instead of corrupting memory. - A
Linkerbuilds a callable handle to a native function, mapping C types to Java types up front.
The result is that mistakes surface as Java exceptions at link time, not as random crashes later. The rest of this chapter walks through each piece in turn.
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. If your code uses virtual threads, prefer ofShared() or ofAuto(), since a confined arena is tied to one carrier thread.
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().
When to Use FFM
FFM is a specialist tool, not an everyday one. Reach for it when you genuinely need native interop or off-heap memory:
- Calling an existing native library — a C image codec, a database driver, a hardware SDK — without writing JNI glue.
- Sharing large buffers with native code where copying onto the Java heap would be wasteful, such as graphics or audio pipelines.
- Working with very large off-heap data sets that should not pressure the garbage collector.
For ordinary file and buffer work, stay with higher-level APIs like Java NIO; they are simpler and safe by default. And remember that FFM is a restricted operation: because native calls escape the JVM's safety guarantees, you must launch with --enable-native-access or you will get a runtime warning or error.