The Foreign Function and Memory API, finalized in JDK 22 by JEP 454, lets Java code allocate memory outside the heap, read and write it with checked, typed accessors, and call C functions directly, all in plain Java and without writing a line of JNI glue. It is the core deliverable of Project Panama.

Its design is small once you see it: arenas own memory, segments describe it, layouts give it shape, and the linker turns a C function signature into a MethodHandle. This article builds up each piece with runnable code, explains the lifetime rules that turn native crashes into Java exceptions, and ends with a worked example and a checklist. The history, the comparison with JNI and jextract, native-access policy and packaging are covered in the Project Panama overview; this page is about using the API itself.

Advertisement

Two halves of one API

The memory half answers "how do I hold and access bytes that the garbage collector does not manage?" The function half answers "how do I call a C function, or let C call me?" They meet at MemorySegment: every pointer passed to or returned from native code is a segment, and every segment has a lifetime owned by an arena.

That shared lifetime is the point. With raw pointers, as in sun.misc.Unsafe or JNI, a use after free silently corrupts memory. With the FFM API, every access checks liveness, bounds and thread, so the same mistake becomes an exception.

The FFM API: memory access and function calls, both bounded by an arenaArenaconfined, shared, auto, globalMemorySegmentaddress + size + scopeMemoryLayoutstruct, sequence, paddingVarHandletyped, bounds-checked accessSymbolLookuplibrary or defaultLinkernative ABIDowncall handleJava calls CUpcall stubC calls JavaNative librarylibc, your .so / .dllallocatesshapesderivesaddressFunctionDescriptorinvokecallbackstub lifetimeargumentsEvery segment and every upcall stub belongs to an arena; closing the arena ends them all at once.
Arenas own segments and upcall stubs; layouts describe segment contents and produce VarHandles; the Linker binds symbols found by a SymbolLookup into downcall handles and turns Java method handles into upcall stubs.

Arenas: who frees memory, and when

An Arena is a lifetime. Everything allocated from it becomes invalid together when it closes, which replaces thousands of individual free calls with one scope. There are four kinds:

ArenaCloseThreadsUse for
Arena.ofConfined()explicit, try-with-resourcesowner thread onlymost request- or call-scoped native work
Arena.ofShared()explicit, any threadany threadbuffers shared by worker threads
Arena.ofAuto()none; the GC frees it when unreachableany threadlong-lived memory with no clear owner
Arena.global()neverany threadprocess-lifetime tables and constants
import java.lang.foreign.*;
import static java.lang.foreign.ValueLayout.*;

try (Arena arena = Arena.ofConfined()) {            // freed at the end of the block
    MemorySegment buf = arena.allocate(JAVA_INT, 1024); // 4 KiB, zeroed, 4-byte aligned
    for (int i = 0; i < 1024; i++) {
        buf.setAtIndex(JAVA_INT, i, i * i);
    }
    MemorySegment tail = buf.asSlice(4000);          // bytes 4000..4095, same lifetime
    int last = tail.get(JAVA_INT, 92);               // index 1023
    buf.get(JAVA_INT, 4096);                          // IndexOutOfBoundsException, not a crash
}
// Using buf here throws IllegalStateException: the scope is closed.

Confined arenas are the default choice: closing is cheap and access checks are simplest. Closing a shared arena must make sure no other thread is mid-access, which makes close more expensive, so do not open and close shared arenas per call. An automatic arena gives up deterministic release, which is exactly the problem with direct ByteBuffer memory, so reserve it for cases with no natural owner. Accessing a confined segment from another thread throws WrongThreadException; accessing any segment after its arena closes throws IllegalStateException.

Advertisement

Segments: bounds, slices and zero-length pointers

A MemorySegment is an address, a size and a scope. Every access is checked against the size, and slices made with asSlice share the parent's scope, so a slice cannot outlive its memory. Segments can also wrap heap arrays with MemorySegment.ofArray, which lets the same code run over on-heap and off-heap data.

Pointers returned by native code are different. The JVM cannot know how large the memory behind a char* is, so it gives you a zero-length segment in the global scope: any read throws. You have two ways to give it a size. If the target type is known statically, declare the pointer layout with a target, as in ADDRESS.withTargetLayout(JAVA_INT), and the returned segment is sized for that layout. If the size is only known at run time, call segment.reinterpret(size), optionally with an arena and a cleanup action so the memory is freed with that arena. reinterpret is a restricted method: if you claim the wrong size, the JVM can read memory it should not; see the native-access policy in the overview article.

Layouts and VarHandles: describing a C struct

A MemoryLayout describes the shape of memory: value layouts such as JAVA_INT and ADDRESS, struct layouts, sequence layouts for arrays, and padding layouts. Layouts carry alignment, and the struct layout constructor checks that each member sits at an offset matching its alignment. If the C compiler would insert padding, you must insert a paddingLayout yourself, which forces you to think about the ABI rather than guessing.

// C:  struct point { int32_t x; int32_t y; double weight; };   // 16 bytes, 8-aligned
static final StructLayout POINT = MemoryLayout.structLayout(
        JAVA_INT.withName("x"),
        JAVA_INT.withName("y"),
        JAVA_DOUBLE.withName("weight"));

static final VarHandle X = POINT.varHandle(MemoryLayout.PathElement.groupElement("x"));
static final VarHandle W = POINT.varHandle(MemoryLayout.PathElement.groupElement("weight"));

MemorySegment pts = arena.allocate(POINT, 100);      // array of 100 structs
for (long i = 0; i < 100; i++) {
    long base = i * POINT.byteSize();                 // offset of element i
    X.set(pts, base, (int) i);                        // (segment, base offset, value)
    W.set(pts, base, 1.0 / (i + 1));
}

A layout gives you a VarHandle for any named path. Since JDK 22, VarHandles derived from layouts take an extra long base-offset coordinate after the segment, which is how one handle addresses the same field in every element of an array. Create layouts and handles once, in static final fields; the JIT then treats them as constants and compiles field access down to a bounds check and a load. For whole arrays, MemorySegment.copy and toArray move data between heap arrays and segments in bulk.

Downcalls: calling C from Java

Calling a native function takes three steps. Find its address with a SymbolLookup: linker.defaultLookup() for the C library, or SymbolLookup.libraryLookup(path, arena) for your own library. Describe its signature with a FunctionDescriptor of layouts. Ask the Linker for a downcall handle, a MethodHandle that follows the platform's calling convention.

Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
        linker.defaultLookup().find("strlen").orElseThrow(),
        FunctionDescriptor.of(JAVA_LONG, ADDRESS));   // size_t strlen(const char*)

try (Arena arena = Arena.ofConfined()) {
    MemorySegment cstr = arena.allocateFrom("Hello, Panama");   // UTF-8, NUL-terminated
    long n = (long) strlen.invokeExact(cstr);                  // 13
}

// Variadic: int printf(const char*, ...) called with one int after the format
MethodHandle printf = linker.downcallHandle(
        linker.defaultLookup().find("printf").orElseThrow(),
        FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT),
        Linker.Option.firstVariadicArg(1));

Use invokeExact with exactly matching casts; a mismatch is a WrongMethodTypeException rather than a silently converted argument. Keep handles in static finals so they are created once. Variadic functions such as printf need Linker.Option.firstVariadicArg with the index of the first variadic parameter, and one handle per combination of argument types; C's default promotions apply, so pass a double where C would promote a float. For large headers, the jextract tool generates these bindings from C headers, and code that calls restricted methods should be run with --enable-native-access for its module; both are covered in the overview article.

Capturing errno

Many C functions report failure by returning -1 and setting errno.

The JVM itself may call functions that overwrite errno between your call and your check, so reading it with a second downcall is unreliable. The linker solves this by saving the value immediately after the call into a segment you provide.

// Capture errno immediately after the call, before the JVM can overwrite it.
Linker.Option ccs = Linker.Option.captureCallState("errno");
StructLayout stateLayout = Linker.Option.captureStateLayout();
VarHandle ERRNO = stateLayout.varHandle(MemoryLayout.PathElement.groupElement("errno"));

MethodHandle open = linker.downcallHandle(
        linker.defaultLookup().find("open").orElseThrow(),
        FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT), ccs);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment state = arena.allocate(stateLayout);
    int fd = (int) open.invokeExact(state, arena.allocateFrom("/no/such/file"), 0);
    if (fd < 0) {
        int errno = (int) ERRNO.get(state, 0L);       // 2 (ENOENT) on Linux
    }
}

The capture segment becomes an extra first parameter of the handle. The layout returned by captureStateLayout is platform specific; on Windows it also offers GetLastError and WSAGetLastError.

Upcalls: letting C call Java

Some C APIs take function pointers: sort comparators, event callbacks, allocator hooks. An upcall stub is a native function pointer that, when called, invokes a Java MethodHandle. The classic demonstration is qsort from the C library sorting a native array with a Java comparator.

// void qsort(void* base, size_t n, size_t size, int (*cmp)(const void*, const void*))
static int compareInts(MemorySegment a, MemorySegment b) {
    return Integer.compare(a.get(JAVA_INT, 0), b.get(JAVA_INT, 0));
}

MethodHandle qsort = linker.downcallHandle(
        linker.defaultLookup().find("qsort").orElseThrow(),
        FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS));

AddressLayout INT_PTR = ADDRESS.withTargetLayout(JAVA_INT);   // pointers we may dereference
MethodHandle cmp = MethodHandles.lookup().findStatic(Sorter.class, "compareInts",
        MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class));

try (Arena arena = Arena.ofConfined()) {
    MemorySegment cmpPtr = linker.upcallStub(cmp,
            FunctionDescriptor.of(JAVA_INT, INT_PTR, INT_PTR), arena);
    MemorySegment data = arena.allocateFrom(JAVA_INT, 5, 3, 9, 1, 7);
    qsort.invokeExact(data, 5L, JAVA_INT.byteSize(), cmpPtr);
    int[] sorted = data.toArray(JAVA_INT);           // [1, 3, 5, 7, 9]
}

Three rules apply. The stub belongs to the arena you pass, and once that arena closes the pointer is dangling; if the native library keeps the callback, for example a logging hook, allocate the stub from an arena that lives as long as the registration. An exception thrown out of an upcall cannot unwind through C frames, so the JVM terminates; catch everything inside the Java method and convert it to a return code. Upcalls run on whatever thread the C code uses, so Java code inside them must be thread safe.

Heap segments and critical functions

By default, only native segments may be passed to a downcall, because the garbage collector can move heap arrays. For very short functions, Linker.Option.critical(true) marks the call as critical and allows heap segments as arguments, which avoids copying an array into native memory and back. The option is a promise: the function must run briefly in every case and must never call back into Java, since heap arrays must stay where they are for the whole call. Breaking the promise can cost throughput or crash the JVM, and the option cannot be combined with captureCallState. Hashes, checksums and SIMD kernels over a small array are the intended case; for data-parallel loops that can stay in Java, the Vector API is often the simpler route.

Worked example: a monotonic clock through clock_gettime

This example combines a struct layout, an output pointer and a return code. clock_gettime fills a struct timespec that the caller allocates. On 64-bit Linux both fields are 64-bit, so the layout is two longs with no padding. The value of CLOCK_MONOTONIC is 1 on Linux; it is an operating system constant, which is exactly the kind of detail jextract reads from headers so you do not hard-code it.

// int clock_gettime(clockid_t clk, struct timespec* tp);  Linux x86-64 / AArch64:
// struct timespec { time_t tv_sec; long tv_nsec; }  -> two 64-bit fields
static final StructLayout TIMESPEC = MemoryLayout.structLayout(
        JAVA_LONG.withName("tv_sec"), JAVA_LONG.withName("tv_nsec"));
static final VarHandle SEC  = TIMESPEC.varHandle(MemoryLayout.PathElement.groupElement("tv_sec"));
static final VarHandle NSEC = TIMESPEC.varHandle(MemoryLayout.PathElement.groupElement("tv_nsec"));
static final int CLOCK_MONOTONIC = 1;                // Linux value; not portable

static final MethodHandle CLOCK_GETTIME = Linker.nativeLinker().downcallHandle(
        Linker.nativeLinker().defaultLookup().find("clock_gettime").orElseThrow(),
        FunctionDescriptor.of(JAVA_INT, JAVA_INT, ADDRESS));

static long monotonicNanos() throws Throwable {
    try (Arena arena = Arena.ofConfined()) {
        MemorySegment ts = arena.allocate(TIMESPEC);
        int rc = (int) CLOCK_GETTIME.invokeExact(CLOCK_MONOTONIC, ts);
        if (rc != 0) throw new IllegalStateException("clock_gettime failed");
        return (long) SEC.get(ts, 0L) * 1_000_000_000L + (long) NSEC.get(ts, 0L);
    }
}

Walk through what happens. The confined arena allocates 16 bytes aligned for the struct. The downcall passes the segment's address to C, which writes both fields. The VarHandles read them with bounds and liveness checks. When the try block ends, the memory is freed, and any accidental reference kept to ts now throws instead of reading freed memory. In a hot loop, reuse one segment per thread.

Failure modes

  • Wrong layout. A layout that disagrees with the C struct, such as a missing padding field or a long that is 32 bits on Windows, reads garbage. Generate layouts with jextract or check sizeof and offsetof in a small C test.
  • Dangling callbacks. An upcall stub from a closed arena, still registered in a library, crashes the process when called. Match stub lifetime to registration lifetime.
  • Arena churn. Creating a shared arena per call makes every close a costly synchronization. Use confined arenas per call or long-lived arenas with slicing allocators.
  • Unbounded reinterpret. reinterpret(Long.MAX_VALUE) switches the safety net off. Size to what the API documents.
  • Exceptions in upcalls. An uncaught exception inside a callback terminates the JVM.
  • Off-heap memory invisible to heap tuning. Native allocations do not count toward -Xmx; container memory limits must leave room for them. See JVM memory for the full budget.
  • Shared-memory races. Segments shared between threads have no implicit ordering; use VarHandle access modes such as setRelease and getAcquire, which follow the Java memory model.

Trade-offs

The FFM API makes native access safe by default and fast when handles are static, and removes a C build step compared with JNI. What remains is the ABI itself: layouts are per platform, variadic calls need one handle per shape, and restricted methods still let you lie about sizes. Hand-write bindings for a handful of functions; generate them for a large library. For pure off-heap data structures that never call C, the memory half alone replaces Unsafe and direct buffers with checked, deterministic memory.

What to do next

  1. Rewrite one direct ByteBuffer or Unsafe use in your code with a confined arena and a segment, and confirm out-of-bounds access now throws.
  2. Call strlen and qsort from the default lookup, as above, to learn downcalls and upcalls end to end.
  3. For one C struct you use, write the layout by hand, then compare its byteSize() with sizeof from a C test program.
  4. Add captureCallState("errno") to every handle whose C function reports errors through errno.
  5. Move all layouts, VarHandles and MethodHandles into static final fields and benchmark with JMH before and after.
  6. Audit every upcall stub's arena against how long the native side holds the pointer.
Key takeaway: The Foreign Function and Memory API gives Java checked access to native memory and direct calls to native code. Arenas define lifetimes, segments carry bounds and scope, layouts describe structs and produce VarHandles, and the Linker turns function descriptors into downcall handles and Java methods into upcall stubs. Keep handles static, use confined arenas by default, capture errno at the call, match callback lifetimes to their registrations, and treat reinterpret and critical as promises you must keep.