Java Foreign Memory API: MemorySegment, Arena, and MemoryLayout (Java 22+)

The Foreign Function and Memory (FFM) API, finalized in Java 22 (JEP 454), provides a safe, efficient way to access memory outside the Java heap: both Java-allocated off-heap memory and native C/C++ memory. MemorySegment represents a contiguous region of memory with bounds and lifetime checking. Arena controls the lifetime (allocation scope). MemoryLayout describes the structure of memory regions. Together they eliminate the need for sun.misc.Unsafe for off-heap access.

Contents

Why Foreign Memory?

Java heap memory is managed by the garbage collector: convenient, but every object you allocate adds pressure to the GC. For workloads that process gigabytes of data, require ultra-low latency, or need to share memory with native C/C++ libraries, the GC overhead is a real problem. Foreign memory, memory that lives outside the Java heap, solves this at the cost of requiring explicit lifetime management.

Before the FFM API the options were all painful:

  • ByteBuffer.allocateDirect(): limited to 2 GB, no struct support, no deterministic release.
  • sun.misc.Unsafe: works but bypasses all safety checks and is not a public API.
  • JNI: requires writing C/C++ glue code, complex build setup, and is error-prone.

The FFM API (java.lang.foreign, finalized in Java 22 via JEP 454) provides a safe, fully-public alternative. It adds bounds checking on every access, deterministic release via Arena.close(), and a structured layout system for describing C-struct-like memory regions.

Java
// Main use cases for foreign (off-heap) memory:
// 1. Large datasets (GB-scale) that would pressure the GC
// 2. Zero-copy I/O — memory-mapped files without copying to heap
// 3. Interoperability with native libraries (BLAS, OpenSSL, SQLite, etc.)
// 4. Latency-sensitive systems where GC pauses are unacceptable

import java.lang.foreign.*;   // MemorySegment, Arena, MemoryLayout, ValueLayout
import java.lang.invoke.*;    // VarHandle, MethodHandle (used with layouts and downcalls)
The FFM API is in the java.lang.foreign package (no extra dependency). It replaced the incubator jdk.incubator.foreign module from earlier Java versions.

MemorySegment: Allocating and Reading Memory

A MemorySegment represents a contiguous region of memory: off-heap, heap-backed, or mapped from a file. You allocate off-heap segments through an Arena, then read and write values using ValueLayout constants that specify both the Java type and the byte width.

Java
import java.lang.foreign.*;

// Allocate off-heap memory — 100 bytes
try (Arena arena = Arena.ofConfined()) {
    MemorySegment seg = arena.allocate(100); // 100 bytes, zeroed

    // Write values at byte offsets
    seg.set(ValueLayout.JAVA_INT,    0, 42);         // write int at offset 0
    seg.set(ValueLayout.JAVA_DOUBLE, 8, 3.14);       // write double at offset 8
    seg.set(ValueLayout.JAVA_BYTE,  16, (byte) 0xFF);// write byte at offset 16

    // Read values
    int    i = seg.get(ValueLayout.JAVA_INT,    0);  // 42
    double d = seg.get(ValueLayout.JAVA_DOUBLE, 8);  // 3.14
    System.out.println(i + " " + d);

    // Heap segment — wraps a Java array as a MemorySegment
    int[] arr = {1, 2, 3, 4, 5};
    MemorySegment heapSeg = MemorySegment.ofArray(arr);
    int first = heapSeg.get(ValueLayout.JAVA_INT, 0); // 1

    // Slice — sub-region of a segment
    MemorySegment slice = seg.asSlice(8, 8); // 8 bytes starting at offset 8
    double dSlice = slice.get(ValueLayout.JAVA_DOUBLE, 0); // 3.14

    // Fill — set all bytes to a value
    seg.fill((byte) 0); // zero all bytes

    // Copy between segments
    MemorySegment dest = arena.allocate(8);
    MemorySegment.copy(seg, 0, dest, 0, 8); // copy 8 bytes from seg to dest

    // Segment size and address
    System.out.println(seg.byteSize()); // 100
}
// Memory automatically freed when Arena closes
All memory segment accesses include bounds checking. Accessing outside the segment's bounds throws IndexOutOfBoundsException, not a native crash. This makes foreign memory much safer than Unsafe.

Arena: Memory Lifetime Management

An Arena is the scope that owns and releases off-heap memory. When you allocate a segment through an arena, the segment is valid for exactly as long as that arena is open. Closing the arena frees all memory it owns in one shot: no GC needed, no finalizers.

Java 22 provides four arena types, each with a different lifetime policy:

Java
// Arena controls when allocated memory is freed
// Different Arena types for different lifetime needs:

// 1. Confined Arena — single-threaded, freed on close() — most common
try (Arena arena = Arena.ofConfined()) {
    MemorySegment s1 = arena.allocate(100);
    MemorySegment s2 = arena.allocate(200);
    // both s1 and s2 freed when arena closes
} // freed here

// 2. Shared Arena — multi-threaded, freed on close()
try (Arena arena = Arena.ofShared()) {
    MemorySegment shared = arena.allocate(1000);
    // Multiple threads can access 'shared' safely
    // Freed when arena closes
}

// 3. Auto Arena — freed by GC (when no references remain)
// No try-with-resources needed, but non-deterministic release
MemorySegment autoSeg = Arena.ofAuto().allocate(100);
// Freed when autoSeg is GC'd — useful for long-lived memory

// 4. Global Arena — memory lives for JVM lifetime
// For static native buffers that must always exist
MemorySegment globalSeg = Arena.global().allocate(1024);
// Never freed — use sparingly

// Checking if a segment is still alive
Arena confined = Arena.ofConfined();
MemorySegment seg = confined.allocate(10);
System.out.println(seg.isNative());       // true — off-heap
confined.close();
// seg.get(ValueLayout.JAVA_INT, 0); // IllegalStateException — segment freed!

// Allocate with alignment (important for SIMD instructions)
try (Arena arena = Arena.ofConfined()) {
    MemorySegment aligned = arena.allocate(64, 64); // 64 bytes, 64-byte aligned
}

MemoryLayout: Structured Memory

Raw byte offsets work for trivial cases, but when you need to model a C struct you want something declarative. MemoryLayout lets you describe the structure of a memory region (field names, types, padding, and total size) in Java code. The API then derives byte offsets for you and lets you create type-safe VarHandle accessors for each field.

Java
import java.lang.foreign.*;
import java.lang.invoke.*;

// MemoryLayout describes the structure of a memory segment (like a C struct)
// Think: sizeof() and offsetof() from C, but declarative

// Simple C-like struct: { int x; int y; double z; }
StructLayout pointLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("x"),
    ValueLayout.JAVA_INT.withName("y"),
    ValueLayout.JAVA_DOUBLE.withName("z")
);

System.out.println(pointLayout.byteSize()); // 16 bytes (4 + 4 + 8)

// VarHandle — type-safe accessor for a named field
VarHandle xHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("y"));
VarHandle zHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("z"));

try (Arena arena = Arena.ofConfined()) {
    MemorySegment seg = arena.allocate(pointLayout);

    xHandle.set(seg, 0L, 10);    // set x=10 at first element (index 0)
    yHandle.set(seg, 0L, 20);    // set y=20
    zHandle.set(seg, 0L, 3.14);  // set z=3.14

    int  x = (int)    xHandle.get(seg, 0L); // 10
    int  y = (int)    yHandle.get(seg, 0L); // 20
    double z = (double) zHandle.get(seg, 0L); // 3.14
}

// Sequence layout — array of structs
SequenceLayout arrayLayout = MemoryLayout.sequenceLayout(10, pointLayout); // 10 points
System.out.println(arrayLayout.byteSize()); // 160 bytes

// Padding — add explicit padding bytes (for alignment)
StructLayout paddedLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("id"),
    MemoryLayout.paddingLayout(4),  // 4 bytes padding to align next double
    ValueLayout.JAVA_DOUBLE.withName("value")
);

Calling Native Functions

The FFM API can call native C functions directly from Java: no JNI, no C glue code. You describe the function signature with a FunctionDescriptor, look up the native symbol with SymbolLookup, and ask the Linker to produce a MethodHandle you invoke like any other Java method. Going the other direction, an upcall stub lets native code call back into Java.

Java
import java.lang.foreign.*;
import java.lang.invoke.*;

// SymbolLookup — find native library functions by name
// Linker — links Java MethodHandle to native function

Linker linker = Linker.nativeLinker();

// Find strlen from the C standard library
SymbolLookup stdlib = linker.defaultLookup();
MemorySegment strlenAddr = stdlib.find("strlen").orElseThrow();

// Describe the function signature: size_t strlen(const char* s)
FunctionDescriptor strlenDesc = FunctionDescriptor.of(
    ValueLayout.JAVA_LONG,   // return type: size_t
    ValueLayout.ADDRESS       // arg: const char*
);

// Create a MethodHandle to call strlen
MethodHandle strlen = linker.downcallHandle(strlenAddr, strlenDesc);

// Use it
try (Arena arena = Arena.ofConfined()) {
    MemorySegment cStr = arena.allocateFrom("Hello, World!"); // null-terminated C string
    long len = (long) strlen.invoke(cStr);
    System.out.println("strlen = " + len); // 13
}

// Upcall — Java code called from native (callback)
// FunctionDescriptor describes the callback signature
// MethodHandles.lookup().findStatic(...) provides the Java method
// linker.upcallStub(handle, descriptor, arena) creates a native function pointer

// LoadLibrary — load a native .so / .dll
System.loadLibrary("mylib"); // loads libmylib.so / mylib.dll
SymbolLookup myLib = SymbolLookup.loaderLookup();
// find and call functions from myLib as above
The FFM API (java.lang.foreign) is powerful but low-level. Incorrect use of pointer arithmetic or mismatched layouts can cause native crashes. Always test thoroughly and validate memory layouts against the actual native library headers.