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:
: limited to 2 GB, no struct support, no deterministic release.ByteBuffer.allocateDirect() : works but bypasses all safety checks and is not a public API.sun.misc.Unsafe- JNI: requires writing C/C++ glue code, complex build setup, and is error-prone.
The FFM API (java.lang.foreignArena.close()
// 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)
java.lang.foreignjdk.incubator.foreignMemorySegment: Allocating and Reading Memory
A MemorySegmentArenaValueLayout
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
IndexOutOfBoundsExceptionUnsafeArena: Memory Lifetime Management
An Arena
Java 22 provides four arena types, each with a different lifetime policy:
// 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. MemoryLayoutVarHandle
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 FunctionDescriptorSymbolLookupLinkerMethodHandle
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