Lesson 49 of 71
The Foreign Function and Memory API
The Foreign Function and Memory (FFM) API in java.lang.foreign reads and writes memory outside the Java heap and calls native functions without JNI. It became final in Java 22 (JEP 454). It fixes the two gaps of ByteBuffer and memory-mapped files: memory is freed at a known point, and segments are indexed by long, not int. After this lesson, a reader can allocate and lay out off-heap data, map files with deterministic unmapping, and call a C function.
| API | Purpose |
|---|---|
Arena |
Controls the lifetime of memory: when it is freed and which threads may use it |
MemorySegment |
A bounded region of memory, on or off the heap |
ValueLayout |
Size, alignment, and byte order of one value (JAVA_INT, JAVA_LONG, ADDRESS) |
MemoryLayout.structLayout |
Named fields with offsets, like a C struct |
FileChannel.map(..., arena) |
A mapped file as a segment, unmapped when the arena closes |
Linker, FunctionDescriptor |
Calls to native functions (downcalls) |
Example 1: allocate, write, read, and bounds
An arena allocates segments. A confined arena (Arena.ofConfined()) belongs to the thread that created it and frees all its memory when close runs, so try-with-resources gives it a clear lifetime. Every access is checked against the segment's size.
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
public class Main {
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment prices = arena.allocate(ValueLayout.JAVA_INT, 5);
for (int i = 0; i < 5; i++) {
prices.setAtIndex(ValueLayout.JAVA_INT, i, 100 + i);
}
long sum = 0;
for (int i = 0; i < 5; i++) {
sum += prices.getAtIndex(ValueLayout.JAVA_INT, i);
}
System.out.println("bytes " + prices.byteSize() + " sum " + sum);
System.out.println("native: " + prices.isNative());
try {
prices.getAtIndex(ValueLayout.JAVA_INT, 5);
} catch (IndexOutOfBoundsException e) {
System.out.println("out of bounds access rejected");
}
}
}
}bytes 20 sum 510
native: true
out of bounds access rejectedallocate(JAVA_INT, 5) returned 20 bytes, zero-filled and aligned for int. getAtIndex multiplies the index by the layout size; get(layout, byteOffset) takes a byte offset instead. Reading index 5 threw instead of reading the memory after the segment, which is what sun.misc.Unsafe would do.
Example 2: the four arena kinds
The arena kind decides who may access a segment and when it is freed. Access after the arena closes throws IllegalStateException, and a confined segment used from another thread throws WrongThreadException.
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
public class Main {
public static void main(String[] args) throws Exception {
MemorySegment leaked;
try (Arena confined = Arena.ofConfined()) {
MemorySegment s = confined.allocate(8);
leaked = s;
Thread t = new Thread(() -> {
try {
s.get(ValueLayout.JAVA_LONG, 0);
} catch (WrongThreadException e) {
System.out.println("confined: other thread rejected");
}
});
t.start();
t.join();
}
try {
leaked.get(ValueLayout.JAVA_LONG, 0);
} catch (IllegalStateException e) {
System.out.println("confined: access after close rejected");
}
try (Arena shared = Arena.ofShared()) {
MemorySegment s = shared.allocate(8);
Thread t = new Thread(() -> s.set(ValueLayout.JAVA_LONG, 0, 99L));
t.start();
t.join();
System.out.println("shared: value from other thread " + s.get(ValueLayout.JAVA_LONG, 0));
}
MemorySegment auto = Arena.ofAuto().allocate(8); // freed by GC when unreachable
auto.set(ValueLayout.JAVA_LONG, 0, 1L);
System.out.println("auto: usable " + (auto.get(ValueLayout.JAVA_LONG, 0) == 1L));
try {
Arena.global().close();
} catch (UnsupportedOperationException e) {
System.out.println("global and auto arenas cannot be closed");
}
}
}confined: other thread rejected
confined: access after close rejected
shared: value from other thread 99
auto: usable true
global and auto arenas cannot be closed| Kind | Threads | Freed | Typical use |
|---|---|---|---|
ofConfined() |
Owner thread only | On close |
Per-request or per-call scratch memory |
ofShared() |
Any thread | On close |
Memory shared by worker threads |
ofAuto() |
Any thread | By the GC, when no segment is reachable | Replacement for direct ByteBuffer |
global() |
Any thread | Never | Memory that lives as long as the process |
Closing a shared arena is slower than closing a confined one, because the JVM must make sure no other thread is in the middle of an access (HotSpot uses a handshake with the other threads).
Example 3: struct layouts and VarHandle access paths
MemoryLayout.structLayout describes fields in order with their names. The layout computes offsets and total size, and varHandle(PathElement.groupElement(name)) returns a VarHandle (the typed accessor from VarHandles and Fences) for one field. The handle takes the segment and a base offset as its first two coordinates.
import java.lang.foreign.Arena;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemoryLayout.PathElement;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.StructLayout;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.VarHandle;
public class Main {
static final StructLayout ORDER = MemoryLayout.structLayout(
ValueLayout.JAVA_LONG.withName("id"),
ValueLayout.JAVA_INT.withName("qty"),
ValueLayout.JAVA_INT.withName("price"));
static final VarHandle ID = ORDER.varHandle(PathElement.groupElement("id"));
static final VarHandle QTY = ORDER.varHandle(PathElement.groupElement("qty"));
static final VarHandle PRICE = ORDER.varHandle(PathElement.groupElement("price"));
public static void main(String[] args) {
System.out.println("struct size " + ORDER.byteSize() + ", alignment " + ORDER.byteAlignment());
System.out.println("price offset " + ORDER.byteOffset(PathElement.groupElement("price")));
try (Arena arena = Arena.ofConfined()) {
MemorySegment order = arena.allocate(ORDER);
ID.set(order, 0L, 7L);
QTY.set(order, 0L, 300);
PRICE.set(order, 0L, 10_050);
System.out.println("id=" + (long) ID.get(order, 0L)
+ " qty=" + (int) QTY.get(order, 0L)
+ " price=" + (int) PRICE.get(order, 0L));
}
}
}struct size 16, alignment 8
price offset 12
id=7 qty=300 price=10050The handles are static final, so the JIT treats them as constants and compiles each access to a plain load or store with a bounds check. The casts on get matter: a VarHandle call with the wrong static types fails at run time with WrongMethodTypeException.
Example 4: arrays of structs and padding
A sequenceLayout repeats an element layout. A path with sequenceElement() adds an index coordinate to the handle. C compilers insert padding so each field sits at an offset that is a multiple of its size; the FFM API does not insert it, so a layout with misaligned fields is rejected and padding must be written with paddingLayout.
import java.lang.foreign.Arena;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemoryLayout.PathElement;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SequenceLayout;
import java.lang.foreign.StructLayout;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.VarHandle;
public class Main {
static final StructLayout TICK = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("symbol"),
MemoryLayout.paddingLayout(4), // align the long to 8
ValueLayout.JAVA_LONG.withName("price"));
static final SequenceLayout TICKS = MemoryLayout.sequenceLayout(4, TICK);
static final VarHandle PRICE = TICKS.varHandle(
PathElement.sequenceElement(), PathElement.groupElement("price"));
public static void main(String[] args) {
System.out.println("tick size " + TICK.byteSize() + ", array size " + TICKS.byteSize());
try {
MemoryLayout.structLayout(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG);
} catch (IllegalArgumentException e) {
System.out.println("struct without padding rejected");
}
try (Arena arena = Arena.ofConfined()) {
MemorySegment ticks = arena.allocate(TICKS);
for (long i = 0; i < 4; i++) PRICE.set(ticks, 0L, i, 100L * (i + 1));
long total = 0;
for (long i = 0; i < 4; i++) total += (long) PRICE.get(ticks, 0L, i);
System.out.println("total price " + total);
}
}
}tick size 16, array size 64
struct without padding rejected
total price 1000Each tick takes 16 bytes: 4 for the symbol, 4 of padding, 8 for the price. Matching a C struct requires writing the same padding the C compiler adds, which is why tools such as jextract generate layouts from header files.
Example 5: mapping a file into an arena
FileChannel.map(mode, offset, size, arena) (Java 22) returns a MemorySegment instead of a MappedByteBuffer. Closing the arena unmaps the file at once, which solves the unmapping problem of MappedByteBuffer, and the segment can be larger than 2 GB.
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class Main {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("ticks", ".dat");
MemorySegment escaped;
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE);
Arena arena = Arena.ofConfined()) {
MemorySegment map = ch.map(FileChannel.MapMode.READ_WRITE, 0, 4096, arena);
for (int i = 0; i < 8; i++) {
map.setAtIndex(ValueLayout.JAVA_LONG, i, 1_000L + i);
}
map.force();
escaped = map;
System.out.println("mapped bytes " + map.byteSize() + ", file size " + ch.size());
}
try {
escaped.get(ValueLayout.JAVA_LONG, 0);
} catch (IllegalStateException e) {
System.out.println("access after unmap rejected");
}
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ);
Arena arena = Arena.ofConfined()) {
MemorySegment ro = ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size(), arena);
long sum = 0;
for (int i = 0; i < 8; i++) sum += ro.getAtIndex(ValueLayout.JAVA_LONG, i);
System.out.println("sum of 8 longs " + sum + ", read-only " + ro.isReadOnly());
}
Files.delete(file);
}
}mapped bytes 4096, file size 4096
access after unmap rejected
sum of 8 longs 8028, read-only trueAfter the arena closed, the old segment threw instead of touching unmapped memory, which with Unsafe.invokeCleaner would crash the process. Values written through one mapping are in the page cache and visible to the next mapping.
Example 6: a downcall to strlen
A downcall is a call from Java into a native function. Linker.nativeLinker() knows the platform's calling convention, defaultLookup() finds symbols in the C standard library, and a FunctionDescriptor gives the return and parameter layouts. arena.allocateFrom(String) copies a Java string into native memory as a NUL-terminated UTF-8 C string.
Calling native code is a restricted operation, because a wrong descriptor can crash the JVM. Without --enable-native-access=ALL-UNNAMED (or a module name), the JVM prints a warning to standard error on the first restricted call; the example below runs with the flag.
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class Main {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateFrom("order-12345");
System.out.println("segment bytes " + cString.byteSize());
System.out.println("strlen " + (long) strlen.invokeExact(cString));
System.out.println("back to Java: " + cString.getString(0));
}
}
}segment bytes 12
strlen 11
back to Java: order-12345The segment has 12 bytes because of the terminating NUL, and strlen counted 11. invokeExact requires the exact static types of the descriptor, so the result is cast to long and the argument is a MemorySegment. A native function that returns a pointer gives back a zero-length segment; reinterpret(size) gives it a size and is also restricted.
Example 7: a realistic off-heap order book side
This program stores price levels in a struct array in native memory, sorted by price, with a binary search over the segment. No objects are created per level, so the GC does not scan or copy the data.
import java.lang.foreign.Arena;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemoryLayout.PathElement;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SequenceLayout;
import java.lang.foreign.StructLayout;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.VarHandle;
public class Main {
static final StructLayout LEVEL = MemoryLayout.structLayout(
ValueLayout.JAVA_LONG.withName("price"),
ValueLayout.JAVA_LONG.withName("qty"));
static final int MAX = 1024;
static final SequenceLayout LEVELS = MemoryLayout.sequenceLayout(MAX, LEVEL);
static final VarHandle PRICE = LEVELS.varHandle(PathElement.sequenceElement(), PathElement.groupElement("price"));
static final VarHandle QTY = LEVELS.varHandle(PathElement.sequenceElement(), PathElement.groupElement("qty"));
final MemorySegment seg;
int size;
Main(Arena arena) { seg = arena.allocate(LEVELS); }
int find(long price) { // binary search, returns insertion point if absent
int lo = 0, hi = size - 1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
long p = (long) PRICE.get(seg, 0L, (long) mid);
if (p < price) lo = mid + 1; else if (p > price) hi = mid - 1; else return mid;
}
return -(lo + 1);
}
void add(long price, long qty) {
int i = find(price);
if (i >= 0) {
QTY.set(seg, 0L, (long) i, (long) QTY.get(seg, 0L, (long) i) + qty);
return;
}
int at = -i - 1;
long bytes = LEVEL.byteSize();
MemorySegment.copy(seg, at * bytes, seg, (at + 1) * bytes, (size - at) * bytes); // shift right
PRICE.set(seg, 0L, (long) at, price);
QTY.set(seg, 0L, (long) at, qty);
size++;
}
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
Main book = new Main(arena);
long[][] orders = {{10_050, 100}, {10_020, 50}, {10_080, 70}, {10_050, 25}, {10_010, 10}};
for (long[] o : orders) book.add(o[0], o[1]);
System.out.println("levels " + book.size + ", memory " + book.seg.byteSize() + " bytes");
for (int i = 0; i < book.size; i++) {
System.out.println(" " + (long) PRICE.get(book.seg, 0L, (long) i)
+ " x " + (long) QTY.get(book.seg, 0L, (long) i));
}
}
}
}levels 4, memory 16384 bytes
10010 x 10
10020 x 50
10050 x 125
10080 x 70MemorySegment.copy handles overlapping source and target ranges, like System.arraycopy, so it can shift levels inside one segment. The whole book is one 16 KB allocation freed when the arena closes.
Comparison with Unsafe and ByteBuffer
| Property | sun.misc.Unsafe |
ByteBuffer (direct, mapped) |
FFM MemorySegment |
|---|---|---|---|
| Bounds checks | None | Yes | Yes |
| Use after free | Crash or silent corruption | Not possible: freed only by GC | IllegalStateException |
| Deterministic free | freeMemory |
No public way | Arena.close |
| Maximum size | long |
int (2 GB) |
long |
| Struct layouts | Manual offsets | Manual offsets | MemoryLayout, VarHandle paths |
| Native calls | No (JNI needed) | No | Linker |
| Status | Memory-access methods deprecated for removal (Java 23) | Supported | Final in Java 22 |
A segment can also wrap a ByteBuffer (MemorySegment.ofBuffer) and a segment can be viewed as one (asByteBuffer, if it is smaller than 2 GB), so code can move to FFM one component at a time.
Interview points
- FFM became final in Java 22 (JEP 454); it replaces
Unsafememory access and most JNI use. - Arena kinds: confined (one thread, close), shared (any thread, close), auto (GC), global (never freed).
- Every segment access checks bounds, lifetime, and thread confinement.
- Struct layouts do not add padding; misaligned fields are rejected, and padding is explicit.
static finalVarHandles from layout paths compile to plain loads and stores.FileChannel.map(..., arena)unmaps onclose, unlikeMappedByteBuffer.- Downcalls and
reinterpretare restricted;--enable-native-accessremoves the warning. invokeExacton a downcall handle needs exact static types.
Summary
Arenaowns memory;MemorySegmentis a bounded view;ValueLayoutdescribes one value.- Access after close throws
IllegalStateException; access from a wrong thread throwsWrongThreadException. structLayout,sequenceLayout, andpaddingLayoutdescribe C-like data.- A mapped segment can exceed 2 GB and is unmapped when its arena closes.
Linker.nativeLinker().downcallHandleturns a native symbol into aMethodHandle.allocateFrom(String)writes a NUL-terminated UTF-8 string.
