Contents
- Why Foreign Memory?
- MemorySegment — Allocating and Reading Memory
- Arena — Memory Lifetime Management
- MemoryLayout — Structured Memory
- Calling Native Functions
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.
// 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.
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.
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.
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:
// 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
}
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.
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")
);
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.
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.