Selama tiga dekade, jalan resmi Java menuju dunia luar adalah JNI: interface yang menuntut berkas header hasil javah, kode C rapuh, dan aturan memori manual yang satu saja salah menyebabkan crash seluruh JVM. Sebagian besar pustaka pun memilih jalur tidak resmi sun.misc.Unsafe — cepat, tetapi sama rapuhnya dan sulit dipindahkan. Foreign Function and Memory API (FFM API), difinalkan di Java 22 melalui JEP 454 setelah empat gelombang incubator dan preview, menggantikan keduanya: memanggil fungsi native lewat MethodHandle, dan mengelola memori luar heap lewat MemorySegment yang masa hidupnya terikat Arena. Chapter ini membahas dua sisi itu dengan contoh yang berjalan langsung pada JDK 25.
MemorySegment dan Arena #
MemorySegment adalah tampilan Java atas area memori berkesinambungan — bisa memori luar heap (off-heap) hasil alokasi, potongan heap, atau pemetaan berkas. Aksesnya dinyatakan lewat ValueLayout (JAVA_INT, JAVA_LONG, ADDRESS) sehingga ofset dan lebar tipe eksplisit. Masa hidup dikunci oleh Arena: blok try menutup arena otomatis, seluruh segmen di dalamnya tidak dapat diakses lagi, dan akses setelah penutupan ditolak dengan IllegalStateException — bukan undefined behavior diam-diam seperti pada Unsafe.
Mengelola Memori Off-Heap #
Contoh teruji berikut mengalokasikan memori luar heap, mengisi slot int, lalu membuktikan penutupan arena benar-benar menutup akses:
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
public class MemoriLuar {
public static void main(String[] args) {
// Arena confined: satu pemilik thread, ditutup otomatis oleh try.
try (Arena arena = Arena.ofConfined()) {
// Alokasi off-heap: 4 slot int (16 byte) di luar heap JVM.
MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT, 4);
seg.set(ValueLayout.JAVA_INT, 0L, 100);
seg.set(ValueLayout.JAVA_INT, 4L, 200);
seg.set(ValueLayout.JAVA_INT, 8L, 300);
seg.set(ValueLayout.JAVA_INT, 12L, 400);
long jumlah = 0;
for (long ofset = 0; ofset < seg.byteSize(); ofset += 4) {
jumlah += seg.get(ValueLayout.JAVA_INT, ofset);
}
System.out.println("ukuran segmen : " + seg.byteSize() + " byte");
System.out.println("jumlah nilai : " + jumlah);
// allocateFrom menyalin array Java ke memori luar sekaligus.
MemorySegment salinan = arena.allocateFrom(
ValueLayout.JAVA_INT, new int[]{7, 8, 9});
System.out.println("salinan[2] : " + salinan.get(ValueLayout.JAVA_INT, 8L));
} // arena.close() di sini: seluruh segmen dilepas sekaligus
// Segmen dari arena tertutup menolak akses dengan exception jelas.
MemorySegment seg = null;
try (Arena arena = Arena.ofConfined()) {
seg = arena.allocate(ValueLayout.JAVA_INT, 1);
}
try {
seg.get(ValueLayout.JAVA_INT, 0L);
} catch (IllegalStateException e) {
System.out.println("setelah close : " + e.getClass().getSimpleName()
+ " (" + e.getMessage() + ")");
}
}
}Output yang Dihasilkan:
ukuran segmen : 16 byte
jumlah nilai : 1000
salinan[2] : 9
setelah close : IllegalStateException (Already closed)Bedah baris-per-baris untuk bagian pentingnya:
- Baris
try (Arena arena = Arena.ofConfined()): arena confined hanya boleh disentuh thread pembuatnya — pilihan paling aman;Arena.ofShared()tersedia bila beberapa thread perlu membaca segmen bersama. - Baris
arena.allocate(ValueLayout.JAVA_INT, 4): memesan empat slotint(16 byte) di luar heap — tidak menyentuh garbage collector, tidak ikut dihitung ukuran heap. - Baris
seg.set(ValueLayout.JAVA_INT, 4L, 200): menulisintpada ofset byte 4; semua akses menyebut layout secara eksplisit sehingga lebar tipe tidak dikira-kira. - Blok terakhir: setelah try-with-resources menutup arena kedua,
get()melemparIllegalStateException (Already closed)— kegagalan terdeteksi dan terdiagnosis, bukan kerusakan senyap.
Memanggil Fungsi Native #
Sisi kedua FFM menghubungkan Java ke pustaka sistem operasi. Linker.nativeLinker() menyiapkan jembatan; FunctionDescriptor menyatakan signature fungsi; hasilnya berupa MethodHandle biasa. Contoh teruji berikut memanggil strlen dari libc melalui pencari simbol bawaan:
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 PanggilNative {
public static void main(String[] args) throws Throwable {
// Linker menghubungkan Java dengan pustaka native sistem operasi.
Linker linker = Linker.nativeLinker();
// strlen dari libc: panjang string C (tanpa sentinel nol).
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").get(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
try (Arena arena = Arena.ofConfined()) {
// Salin teks Java ke memori native dengan penghenti nol.
MemorySegment teks = arena.allocateFrom("Halo dari FFM");
long panjang = (long) strlen.invoke(teks);
System.out.println("teks : " + teks.getString(0));
System.out.println("panjang: " + panjang + " karakter");
}
}
}Output yang Dihasilkan:
teks : Halo dari FFM
panjang: 13 karakterBedah singkatnya: downcallHandle mengubah signature long strlen(char*) menjadi MethodHandle yang menerima MemorySegment; arena.allocateFrom("Halo dari FFM") menyalin string Java ke memori native lengkap dengan penghenti nol versi C; teks.getString(0) membacanya kembali. Kode berjalan pada Java 25 tanpa flag kompilasi tambahan.
JNI Digantikan #
| Aspek | JNI | FFM API |
|---|---|---|
| Bindings | Berkas C hasil generasi, wrapper per platform | MethodHandle langsung dari Java |
| Memori luar | ByteBuffer tidak langsung / Unsafe | MemorySegment + Arena |
| Kesalahan akses | Crash JVM atau korupsi senyap | Exception Java dengan pesan jelas |
| Status | Warisan; penggunaannya dibatasi bertahap | Final sejak Java 22 |
Dengan jalan keluar-masuk memori dan fungsi native yang aman, tersisa satu jalur performa yang belum tersentuh arus utama: komputasi vektor SIMD lewat Vector API — masih incubator, dibahas berikutnya.
Referensi #
- OpenJDK: JEP 454 — Foreign Function & Memory API
- Oracle: Package java.lang.foreign (Java API Specification)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
MemoriLuar.java
Unduh berkasimport java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
public class MemoriLuar {
public static void main(String[] args) {
// Arena confined: satu pemilik thread, ditutup otomatis oleh try.
try (Arena arena = Arena.ofConfined()) {
// Alokasi off-heap: 4 slot int (16 byte) di luar heap JVM.
MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT, 4);
seg.set(ValueLayout.JAVA_INT, 0L, 100);
seg.set(ValueLayout.JAVA_INT, 4L, 200);
seg.set(ValueLayout.JAVA_INT, 8L, 300);
seg.set(ValueLayout.JAVA_INT, 12L, 400);
long jumlah = 0;
for (long ofset = 0; ofset < seg.byteSize(); ofset += 4) {
jumlah += seg.get(ValueLayout.JAVA_INT, ofset);
}
System.out.println("ukuran segmen : " + seg.byteSize() + " byte");
System.out.println("jumlah nilai : " + jumlah);
// allocateFrom menyalin array Java ke memori luar sekaligus.
MemorySegment salinan = arena.allocateFrom(
ValueLayout.JAVA_INT, new int[]{7, 8, 9});
System.out.println("salinan[2] : " + salinan.get(ValueLayout.JAVA_INT, 8L));
} // arena.close() di sini: seluruh segmen dilepas sekaligus
// Segmen dari arena tertutup menolak akses dengan exception jelas.
MemorySegment seg = null;
try (Arena arena = Arena.ofConfined()) {
seg = arena.allocate(ValueLayout.JAVA_INT, 1);
}
try {
seg.get(ValueLayout.JAVA_INT, 0L);
} catch (IllegalStateException e) {
System.out.println("setelah close : " + e.getClass().getSimpleName()
+ " (" + e.getMessage() + ")");
}
}
}
PanggilNative.java
Unduh berkasimport 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 PanggilNative {
public static void main(String[] args) throws Throwable {
// Linker menghubungkan Java dengan pustaka native sistem operasi.
Linker linker = Linker.nativeLinker();
// strlen dari libc: panjang string C (tanpa sentinel nol).
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").get(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
try (Arena arena = Arena.ofConfined()) {
// Salin teks Java ke memori native dengan penghenti nol.
MemorySegment teks = arena.allocateFrom("Halo dari FFM");
long panjang = (long) strlen.invoke(teks);
System.out.println("teks : " + teks.getString(0));
System.out.println("panjang: " + panjang + " karakter");
}
}
}