W3docs

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 here

Every 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.

ArenaLifetimeThread access
Arena.ofConfined()Until close()Only the creating thread
Arena.ofShared()Until close()Any thread
Arena.ofAuto()Until the GC collects itAny thread
Arena.global()The whole programAny 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.

java— editable, runs on the server

What to take from the run:

  • isDirect = true confirms the buffer is allocated off the Java heap — the same property that lets a MemorySegment be passed safely to native code without the GC relocating it.
  • Writing (i + 1) * 10 at each 4-byte offset and reading it back yields 10, 20, 30, 40 with sum = 100, showing that off-heap memory is real, indexable, typed storage just like a MemorySegment.
  • byteSize = 16 is four 4-byte ints — addressing by explicit byte offset is exactly how a ValueLayout computes positions in the real FFM API.
  • The hand-built cString ends in a zero byte, so the strlen-style scan stops there: strlen of the C string = 16 matches Java 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 FFM ofConfined() arena would free deterministically at close().

Practice

Practice

In the FFM API, what is the role of an Arena?