🪞 Reflection, Annotations & Modules · Advanced

Foreign Function & Memory API in Java

Java 22 calling native code and off-heap memory without JNI.

🧩 The mysteryFor decades, calling one C function from Java meant writing C glue code, compiling it per platform and praying it didn't crash. Java 22 made it pure Java.

Beyond JNI

Java 22 finalized the Foreign Function & Memory (FFM) API in java.lang.foreign. It lets Java code use off-heap memory and call native C functions without writing any JNI glue code.

Arena and MemorySegment

An **Arena controls when off-heap memory is freed: close it (try-with-resources) and everything it allocated is released. A MemorySegment is a bounds-checked view** of that memory.

try (Arena arena = Arena.ofConfined()) {
    MemorySegment seg =
        arena.allocate(ValueLayout.JAVA_INT, 3);
    seg.setAtIndex(ValueLayout.JAVA_INT, 0, 7);
} // off-heap memory freed here
🔮 Predict it

Measuring a segment

What does this print?

try (Arena arena = Arena.ofConfined()) {
    MemorySegment seg =
        arena.allocate(ValueLayout.JAVA_LONG, 2);
    seg.setAtIndex(ValueLayout.JAVA_LONG, 1, 5L);
    System.out.println(seg.byteSize());
    System.out.println(
        seg.getAtIndex(ValueLayout.JAVA_LONG, 0));
}
  1. 16 0
  2. 2 0
  3. 16 5
Show the answer

Two 8-byte longs make 16 bytes (three 4-byte ints would make 12). Index 1 got the 5; index 0 was never written, and arena allocations start zeroed, so it reads 0.

⚠️ The trap

No more wild pointers

Every access is checked. Index 3 in a 3-int segment throws **IndexOutOfBoundsException instead of corrupting memory. Using a segment after its arena is closed throws IllegalStateException** instead of reading freed memory.

// 3 ints: valid indexes are 0..2
seg.setAtIndex(JAVA_INT, 3, 9);
// -> IndexOutOfBoundsException
 
// later, after the arena was closed:
seg.get(JAVA_BYTE, 0);
// -> IllegalStateException

Calling C with a Linker

The **Linker finds a native function through a symbol lookup** and turns it into a MethodHandle, given a **FunctionDescriptor (return and parameter layouts). Writing descriptors by hand gets tedious; jextract** generates the bindings from C headers.

Linker linker = Linker.nativeLinker();
var addr = linker.defaultLookup()
        .find("strlen").orElseThrow();
MethodHandle strlen = linker.downcallHandle(
        addr, FunctionDescriptor.of(
            ValueLayout.JAVA_LONG,
            ValueLayout.ADDRESS));
🤔 Think first

Why the warning?

Your FFM code calls a C function, and the JVM prints a warning about restricted methods. Why?

Think about it, then reveal the answer

Linking native functions uses restricted methods: a wrong descriptor can crash the JVM. The JVM warns unless you grant native access explicitly, for example with **--enable-native-access**.

💼 In the real world

Where FFM is used

Native libraries for compression, cryptography, databases or machine learning can now be called from pure Java, with jextract generating the bindings. Big off-heap buffers also stay out of the GC's way, which helps for large caches and I/O.

Key takeaways

  1. Arena: lifetime of off-heap memory (try-with-resources)
  2. MemorySegment checks bounds and lifetime on every access
  3. Linker + SymbolLookup call C functions without JNI
  4. jextract generates Java bindings from C headers
🤯 Did you know?

MemorySegment offsets are long values, so one segment can exceed 2 GB. A ByteBuffer is indexed by int and stops at about 2 GB.

Practice questions

What does this print?

try (Arena arena = Arena.ofConfined()) {
    MemorySegment seg =
        arena.allocate(ValueLayout.JAVA_INT, 3);
    seg.setAtIndex(ValueLayout.JAVA_INT, 1, 42);
    System.out.println(
        seg.getAtIndex(ValueLayout.JAVA_INT, 1));
    System.out.println(seg.byteSize());
}
  1. 42 24
  2. 42 3
  3. 42 12
  4. 0 12
Check your answer

42 12. allocate(JAVA_INT, 3) reserves room for three 4-byte ints, so byteSize() is 12. Index 1 holds the 42 that was just written.

What does this print?

try (Arena arena = Arena.ofConfined()) {
    MemorySegment seg =
        arena.allocate(ValueLayout.JAVA_INT, 3);
    seg.setAtIndex(ValueLayout.JAVA_INT, 3, 9);
    System.out.println("written");
}
  1. Throws IllegalStateException
  2. Throws IndexOutOfBoundsException
  3. Throws NullPointerException
  4. written
Check your answer

Throws IndexOutOfBoundsException. The segment has room for indexes 0 to 2. Writing index 3 would go past the end, so the access is rejected.

That's the end of this world. Next world: Modern Java 17 → 25, where pattern matching, record patterns and sealed types make old Java look verbose.