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:

java MemoriLuar.java
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:

text
ukuran segmen : 16 byte
jumlah nilai  : 1000
salinan[2]    : 9
setelah close : IllegalStateException (Already closed)

Bedah baris-per-baris untuk bagian pentingnya:

  1. 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.
  2. Baris arena.allocate(ValueLayout.JAVA_INT, 4): memesan empat slot int (16 byte) di luar heap — tidak menyentuh garbage collector, tidak ikut dihitung ukuran heap.
  3. Baris seg.set(ValueLayout.JAVA_INT, 4L, 200): menulis int pada ofset byte 4; semua akses menyebut layout secara eksplisit sehingga lebar tipe tidak dikira-kira.
  4. Blok terakhir: setelah try-with-resources menutup arena kedua, get() melempar IllegalStateException (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:

text
teks    : Halo dari FFM
panjang: 13 karakter

Bedah 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 #

AspekJNIFFM API
BindingsBerkas C hasil generasi, wrapper per platformMethodHandle langsung dari Java
Memori luarByteBuffer tidak langsung / UnsafeMemorySegment + Arena
Kesalahan aksesCrash JVM atau korupsi senyapException Java dengan pesan jelas
StatusWarisan; penggunaannya dibatasi bertahapFinal 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

MemoriLuar.java

Unduh berkas
java
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() + ")");
        }
    }
}

PanggilNative.java

Unduh berkas
java
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");
        }
    }
}