W3docs

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 Arena controls the lifetime of memory: when it closes, everything it allocated is freed.
  • A MemorySegment is a bounds-checked view into that memory, so out-of-range access throws an exception instead of corrupting memory.
  • A Linker builds 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 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. 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.

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().

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.

Practice

Practice
In the FFM API, what is the role of an Arena?
In the FFM API, what is the role of an Arena?
Was this page helpful?