Java Foreign Function & Memory API
Nativen Code aufrufen und Off-Heap-Speicher verwalten mit der Foreign Function and Memory API in modernem Java.
Die Foreign Function and Memory (FFM) API ist Javas moderner, sicherer Weg, zwei Dinge zu tun, die früher das fragile Java Native Interface (JNI) erforderten: Funktionen aufzurufen, die in C und anderen nativen Sprachen geschrieben sind, sowie Speicher zu lesen und zu schreiben, der außerhalb des Java-Heaps liegt. Sie wurde in JDK 22 als endgültiges Feature eingeführt und befindet sich im Paket java.lang.foreign.
Dieses Kapitel erklärt, wie Off-Heap-Speicher in FFM funktioniert, wie eine Arena seine Lebensdauer steuert, wie Layouts native Daten beschreiben und wie man eine C-Funktion aus Java heraus aufruft. Am Ende sollten Sie verstehen, wann FFM das richtige Werkzeug ist und wie seine Bausteine zusammenpassen.
Warum FFM JNI ersetzt
Vor FFM erforderte die Kommunikation mit nativem Code handgeschriebenen JNI-Klebe-Code, manuelle Byte-Puffer und ein ständiges Risiko, die JVM mit einem fehlerhaften Zeiger zum Absturz zu bringen. Ein einziger falsch zugeordneter Typ oder ein Off-by-one-Offset konnte den Heap korrumpieren oder den gesamten Prozess zum Absturz bringen — und da der Absturz in nativem Code stattfand, erhielt man keinen Java-Stack-Trace.
FFM ersetzt all das durch eine kleine, typsichere API, die auf drei Konzepten basiert:
- Eine
Arenasteuert die Lebensdauer des Speichers: Wenn sie geschlossen wird, wird alles, was sie zugewiesen hat, freigegeben. - Ein
MemorySegmentist eine grenzengeprüfte Sicht auf diesen Speicher, sodass ein Zugriff außerhalb des Bereichs eine Ausnahme auslöst, anstatt den Speicher zu korrumpieren. - Ein
Linkererstellt ein aufrufbares Handle zu einer nativen Funktion und ordnet C-Typen Java-Typen vorab zu.
Das Ergebnis ist, dass Fehler als Java-Ausnahmen zur Verknüpfungszeit auftauchen, nicht als zufällige Abstürze später. Der Rest dieses Kapitels beschreibt jedes Element im Detail.
Off-Heap-Speicher mit Arena und MemorySegment
Ein MemorySegment ist ein zusammenhängender Speicherbereich mit bekannter Größe. Anders als ein Java-Array kann er außerhalb des Heaps liegen, sodass der Garbage Collector ihn niemals verschiebt und er direkt an nativen Code übergeben werden kann. Sie erstellen ein Segment nie direkt — Sie fragen eine Arena danach, und die Arena besitzt die Lebensdauer des Segments.
Wenn die Arena geschlossen wird, wird jedes von ihr zugewiesene Segment auf einmal freigegeben. Das macht Speicherlecks und Use-after-free-Fehler schwer zu schreiben: Wenn Sie nach dem Schließen der Arena auf ein Segment zugreifen, erhalten Sie eine Ausnahme, keinen Absturz.
import java.lang.foreign.*;
try (Arena arena = Arena.ofConfined()) {
// Allocate room for four ints, off the Java heap.
MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT, 4);
seg.setAtIndex(ValueLayout.JAVA_INT, 0, 100);
int first = seg.getAtIndex(ValueLayout.JAVA_INT, 0);
System.out.println(first); // 100
} // arena.close() frees the segment hereJeder Lese- und Schreibvorgang erfolgt über ein ValueLayout, das genau angibt, wie viele Bytes ein Wert belegt und wie er angeordnet ist. Das sorgt dafür, dass jeder Zugriff grenzengeprüft und typsicher ist.
Eine Arena auswählen
Arena ist der Lebensdauer-Manager, und die von Ihnen gewählte Factory-Methode entscheidet, wer auf den Speicher zugreifen darf und wann er freigegeben wird. Die richtige Wahl zu treffen ist die wichtigste Sicherheitsentscheidung in FFM-Code.
| Arena | Lebensdauer | Thread-Zugriff |
|---|---|---|
Arena.ofConfined() | Bis close() | Nur der erstellende Thread |
Arena.ofShared() | Bis close() | Beliebiger Thread |
Arena.ofAuto() | Bis der GC es einsammelt | Beliebiger Thread |
Arena.global() | Das gesamte Programm | Beliebiger Thread |
Verwenden Sie ofConfined() für den häufigen Fall: kurzlebiger Speicher, der von einem Thread verwendet und deterministisch mit try-with-resources freigegeben wird. Greifen Sie auf ofShared() zurück, nur wenn mehrere Threads dasselbe Segment lesen müssen, und auf ofAuto(), wenn Sie das Ende der Lebensdauer nicht leicht markieren können. Wenn Ihr Code virtuelle Threads verwendet, bevorzugen Sie ofShared() oder ofAuto(), da eine confined Arena an einen Carrier-Thread gebunden ist.
Layouts beschreiben
Ein ValueLayout beschreibt einen einzelnen primitiven Wert; ein MemoryLayout kann ganze Structs und Arrays beschreiben. Layouts ermöglichen es Ihnen, Offsets und Größen zu berechnen, ohne magische Zahlen hartcodieren zu müssen, was den Zugriff auf native Structs lesbar hält.
import java.lang.foreign.*;
import static java.lang.foreign.ValueLayout.*;
// A C struct: struct Point { int x; int y; };
MemoryLayout point = MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment p = arena.allocate(point);
var xHandle = point.varHandle(MemoryLayout.PathElement.groupElement("x"));
var yHandle = point.varHandle(MemoryLayout.PathElement.groupElement("y"));
xHandle.set(p, 0L, 3);
yHandle.set(p, 0L, 4);
System.out.println(xHandle.get(p, 0L) + ", " + yHandle.get(p, 0L)); // 3, 4
}Die benannten Felder und PathElement-Accessoren bedeuten, dass Sie das Struct einmal beschreiben und die API die Byte-Offsets für Sie berechnen lassen.
Native Funktionen mit Linker aufrufen
Das Hauptmerkmal von FFM ist der Downcall: eine C-Funktion aus Java aufrufen. Sie holen den Platform-Linker, suchen die Adresse der Funktion mit einem SymbolLookup, beschreiben ihre Signatur mit einem FunctionDescriptor und erhalten ein MethodHandle, das Sie wie jede Java-Methode aufrufen können.
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
Linker linker = Linker.nativeLinker();
// strlen lives in the standard C library, found via the default lookup.
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
// size_t strlen(const char *s);
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateUtf8String("hello");
long len = (long) strlen.invoke(cString); // 5
}Der FunctionDescriptor ordnet C-Typen Java-Trägern zu: Ein C-Zeiger wird zu ValueLayout.ADDRESS, ein C-size_t wird zu JAVA_LONG, ein C-int zu JAVA_INT. Ordnen Sie die Typen richtig zu und der Aufruf ist typsicher; ordnen Sie sie falsch zu und Sie erfahren es zur Verknüpfungszeit, nicht als zufälligen Absturz. Da native Aufrufe das Sicherheitsnetz der JVM umgehen, ist FFM eine eingeschränkte Operation — das Modul, das sie verwendet, muss mit dem Flag --enable-native-access Zugriff erhalten.
Ein vollständiges, ausführbares Beispiel
Die java.lang.foreign-API ist vor JDK 22 eine Vorschaufunktion, daher führt das folgende Programm dieselben zwei Konzepte — Off-Heap-Speicher und native String-Verarbeitung — nur mit den immer verfügbaren JDK-Klassen aus, die FFM ersetzen sollte. Ein direkter ByteBuffer ist Speicher, der außerhalb des Java-Heaps zugewiesen wird, genau wie ein MemorySegment; das Lesen typisierter Werte an Byte-Offsets spiegelt einen ValueLayout-Zugriff wider; und das Scannen von Bytes bis zu einem Null-Terminator ist genau das, was C's strlen tut.
Was aus dem Durchlauf mitgenommen werden sollte:
isDirect = truebestätigt, dass der Puffer außerhalb des Java-Heaps zugewiesen ist — dieselbe Eigenschaft, die es einemMemorySegmentermöglicht, sicher an nativen Code übergeben zu werden, ohne dass der GC ihn verschiebt.- Das Schreiben von
(i + 1) * 10an jedem 4-Byte-Offset und das Zurücklesen liefert10, 20, 30, 40mitsum = 100, was zeigt, dass Off-Heap-Speicher echter, indizierbarer, typisierter Speicher ist, genau wie einMemorySegment. byteSize = 16sind vier 4-Byte-Ints — die Adressierung durch expliziten Byte-Offset ist genau das, wie einValueLayoutPositionen in der echten FFM API berechnet.- Der handgefertigte
cStringendet in einem Null-Byte, sodass der strlen-artige Scan dort stoppt:strlen of the C string = 16stimmt mitJava String.length() = 16überein und beweist, dass der Null-Terminator das Ende so markiert, wie C es erwartet. - Kein Puffer wird manuell freigegeben — direkte Puffer werden freigegeben, wenn sie unerreichbar sind, was
Arena.ofAuto()widerspiegelt, während die echte FFM-ofConfined()-Arena deterministisch beiclose()freigeben würde.
Wann FFM verwendet werden sollte
FFM ist ein Spezialwerkzeug, kein alltägliches. Greifen Sie darauf zurück, wenn Sie wirklich native Interoperabilität oder Off-Heap-Speicher benötigen:
- Aufrufen einer vorhandenen nativen Bibliothek — ein C-Bild-Codec, ein Datenbanktreiber, ein Hardware-SDK — ohne JNI-Klebe-Code zu schreiben.
- Gemeinsame Nutzung großer Puffer mit nativem Code, wo das Kopieren auf den Java-Heap verschwenderisch wäre, wie bei Grafik- oder Audio-Pipelines.
- Arbeiten mit sehr großen Off-Heap-Datensätzen, die den Garbage Collector nicht belasten sollen.
Für normale Datei- und Pufferarbeiten bleiben Sie bei höherstufigen APIs wie Java NIO; diese sind einfacher und standardmäßig sicher. Und denken Sie daran, dass FFM eine eingeschränkte Operation ist: Da native Aufrufe die Sicherheitsgarantien der JVM umgehen, müssen Sie mit --enable-native-access starten, sonst erhalten Sie eine Laufzeitwarnung oder einen Fehler.