Sebelum Foreign Function and Memory API ada, satu-satunya jalan resmi keluar dari JVM adalah Java Native Interface (JNI) — spesifikasi yang berumur lebih dari dua dekade dan masih menopang banyak pustaka lawas di produksi. JNI memungkinkan kode Java memanggil fungsi C/C++ di dalam berkas .so, .dll, atau .dylib, biasanya demi akses hardware, codec audio-video, atau pustaka matematis yang tidak pernah ditulis ulang ke Java. Memahami JNI tetap penting karena dua alasan: kita akan bertemu pustaka lama yang memakainya, dan beban kerjanya menjelaskan mengapa Java modern merancang penggantinya. Chapter ini membahas anatomi JNI, biaya tersembunyi di setiap panggilan, dan aturan kritis yang membuat kode C di dalam JNI mudah salah — semuanya bisa dipahami tanpa menyentuh compiler C.

Anatomi JNI #

JNI bekerja lewat tiga komponen yang saling terkait. Pertama, method di sisi Java ditandai kata kunci native dan dideklarasikan tanpa body. Kedua, blok static memuat pustaka native lewat System.loadLibrary(). Ketiga, compiler javac dengan opsi -h menghasilkan berkas header C yang mendeklarasikan fungsi dengan nama hasil olahan: prefiks Java_, nama package, nama class, dan nama method digabung dengan garis bawah.

java PustakaAudio.java — sisi Java
public class PustakaAudio {
    static {
        System.loadLibrary("audio");   // memuat libaudio.so / audio.dll
    }

    public static native int hitungRms(byte[] sampel, int panjang);
}

Perintah berikut menghasilkan berkas header C dari class di atas:

bash
javac -h . PustakaAudio.java

Sisi C mengimplementasikan fungsi persis seperti deklarasi di header — berikut bentuk deklarasinya, diambil dari berkas PustakaAudio.h hasil javac -h:

c Deklarasi di PustakaAudio.h
JNIEXPORT jint JNICALL Java_PustakaAudio_hitungRms
  (JNIEnv *, jclass, jbyteArray, jint);

Parameter pertama selalu JNIEnv * — tabel fungsi untuk mengakses kembali objek Java. Parameter kedua jclass adalah class pemanggil. Parameter sisanya adalah argumen asli: jbyteArray mewakili byte[] dan jint mewakili int. Implementasi C yang benar mengikuti pola pin-olah-lepas seperti ini:

c PustakaAudio.c — sisi C
#include "PustakaAudio.h"

JNIEXPORT jint JNICALL Java_PustakaAudio_hitungRms
        (JNIEnv *env, jclass cls, jbyteArray sampel, jint panjang) {
    // 1. "Pin" array Java agar GC tidak memindahkannya selama dipakai.
    jbyte *data = (*env)->GetByteArrayElements(env, sampel, NULL);

    // 2. Olah data di wilayah C. Kode di sini bebas GC, tetapi
    //    setiap akses objek Java harus lewat JNIEnv.

    // 3. Lepaskan pin agar GC bebas lagi bekerja.
    (*env)->ReleaseByteArrayElements(env, sampel, data, 0);
    return 0;
}

Beban di Balik Setiap Panggilan #

Setiap perjalanan Java menuju native dan kembali dikenai biaya tetap: argumen harus dikonversi (marshalling), garbage collector harus diam sebentar saat array di-pin, dan CPU melompat dua arah melewati batas dua dunia dengan konvensi panggilan berbeda. Karena biaya lintas bersifat per-panggilan, jumlah lintasan menentukan total biaya jauh lebih besar daripada berat kerja di sisi C. Program berikut memodelkan prinsip itu dengan aritmetika murni Java:

public class BiayaLintasBatas {
    // Model biaya JNI: setiap perjalanan Java -> native -> Java dikenai
    // biaya tetap, sedangkan biaya olah tiap data selalu 1 satuan.
    static final int BIAYA_LINTAS = 3;
    static final int BIAYA_OLAH = 1;

    public static void main(String[] args) {
        int jumlahData = 1000;

        // Pola buruk: satu panggilan native per data (dua arah per item).
        int perItem = jumlahData * (BIAYA_LINTAS * 2 + BIAYA_OLAH);

        // Pola sehat: satu panggilan batch untuk seluruh data.
        int batch = jumlahData * BIAYA_OLAH + BIAYA_LINTAS * 2;

        System.out.println("model biaya lintas batas JNI");
        System.out.println("jumlah data          : " + jumlahData);
        System.out.println("biaya per lintasan   : " + BIAYA_LINTAS + " satuan");
        System.out.println("biaya olah per data  : " + BIAYA_OLAH + " satuan");
        System.out.println();
        System.out.printf("panggilan per data : %d satuan%n", perItem);
        System.out.printf("panggilan batch    : %d satuan%n", batch);
        System.out.printf("rasio pemborosan   : %.1f kali%n", perItem / (double) batch);
    }
}

Output yang Dihasilkan:

text
model biaya lintas batas JNI
jumlah data          : 1000
biaya per lintasan   : 3 satuan
biaya olah per data  : 1 satuan

panggilan per data : 7000 satuan
panggilan batch    : 1006 satuan
rasio pemborosan   : 7.0 kali

Mari kita telaah hitungannya:

  1. Baris perItem = ...: pola satu-panggilan-per-data membayar dua lintasan (pergi dan pulang) untuk setiap data, sehingga rumusnya 1000 × (3×2 + 1) = 7000 satuan.
  2. Baris batch = ...: pola batch hanya membayar dua lintasan sekali untuk seluruh data, sehingga rumusnya 1000 × 1 + 3×2 = 1006 satuan.
  3. Baris rasio pemborosan: pembagian keduanya menunjukkan pola per-data membayar 7,0 kali lipat biaya pola batch pada model ini — inilah alasan API native yang baik selalu berbentuk olahSemua(sampel), bukan olahSatu(nilai).

Aturan Kritis di Sisi C #

Tabel berikut merangkum kendala JNI yang paling sering menjegal pemrogram baru:

KendalaPenjelasanKonsekuensi bila dilanggar
Critical sectionGetPrimitiveArrayCritical menahan GC selama aktifJVM berhenti total sampai section berakhir
Local referenceReferensi objek Java dari C hanya hidup selama satu panggilanLoop panjang di C menumpuk referensi sampai batasnya terlampaui
Thread attachThread buatan C harus memanggil AttachCurrentThreadAkses objek Java tanpa attach membuat crash
ExceptionGalat di C tidak otomatis menjadi exception JavaStatus galat harus diperiksa manual setelah tiap panggilan

Satu perangkat JNI yang tetap relevan sampai sekarang adalah direct ByteBuffer. Buffer jenis ini dialokasikan di luar heap sehingga dapat diserahkan ke kode native tanpa penyalinan — alasan banyak API audio dan jaringan lawas memakainya:

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class BufferLangsung {
    public static void main(String[] args) {
        ByteBuffer heap = ByteBuffer.allocate(16);
        ByteBuffer langsung = ByteBuffer.allocateDirect(16);

        System.out.println("heap     direct? " + heap.isDirect());
        System.out.println("langsung direct? " + langsung.isDirect());
        System.out.println("punya array belakang? " + langsung.hasArray());

        // Memori direct ditulis sebagai blok byte mentah; endianness
        // harus dinyatakan eksplisit agar sama dengan pihak native.
        ByteBuffer tampilan = langsung.order(ByteOrder.LITTLE_ENDIAN);
        tampilan.putInt(0, 0x0A0B0C0D);
        System.out.println("byte 0 sampai 3     : "
                + String.format("%02X %02X %02X %02X",
                        tampilan.get(0), tampilan.get(1),
                        tampilan.get(2), tampilan.get(3)));
    }
}

Output yang Dihasilkan:

text
heap     direct? false
langsung direct? true
punya array belakang? false
byte 0 sampai 3     : 0D 0C 0B 0A

Bedah singkatnya: allocateDirect(16) memesan 16 byte di luar heap — hasArray() mengembalikan false karena tidak ada array Java di belakangnya; order(ByteOrder.LITTLE_ENDIAN) memastikan urutan byte sama dengan mesin x86 yang lazim dipakai pihak native; nilai 0x0A0B0C0D tersimpan sebagai 0D 0C 0B 0A mulai dari byte pertama.

JNI memang berfungsi, tetapi harga yang kita bayar — header hasil generasi, kode C rawan, GC yang harus dikendalikan manual — terasa semakin mahal. Chapter berikutnya memperkenalkan Foreign Function and Memory API, pengganti resmi JNI sejak Java 22, yang memanggil fungsi native langsung dari Java tanpa satu baris C pun.

Referensi #

Berkas Sumber

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

BiayaLintasBatas.java

Unduh berkas
java
public class BiayaLintasBatas {
    // Model biaya JNI: setiap perjalanan Java -> native -> Java dikenai
    // biaya tetap, sedangkan biaya olah tiap data selalu 1 satuan.
    static final int BIAYA_LINTAS = 3;
    static final int BIAYA_OLAH = 1;

    public static void main(String[] args) {
        int jumlahData = 1000;

        // Pola buruk: satu panggilan native per data (dua arah per item).
        int perItem = jumlahData * (BIAYA_LINTAS * 2 + BIAYA_OLAH);

        // Pola sehat: satu panggilan batch untuk seluruh data.
        int batch = jumlahData * BIAYA_OLAH + BIAYA_LINTAS * 2;

        System.out.println("model biaya lintas batas JNI");
        System.out.println("jumlah data          : " + jumlahData);
        System.out.println("biaya per lintasan   : " + BIAYA_LINTAS + " satuan");
        System.out.println("biaya olah per data  : " + BIAYA_OLAH + " satuan");
        System.out.println();
        System.out.printf("panggilan per data : %d satuan%n", perItem);
        System.out.printf("panggilan batch    : %d satuan%n", batch);
        System.out.printf("rasio pemborosan   : %.1f kali%n", perItem / (double) batch);
    }
}

BufferLangsung.java

Unduh berkas
java
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class BufferLangsung {
    public static void main(String[] args) {
        ByteBuffer heap = ByteBuffer.allocate(16);
        ByteBuffer langsung = ByteBuffer.allocateDirect(16);

        System.out.println("heap     direct? " + heap.isDirect());
        System.out.println("langsung direct? " + langsung.isDirect());
        System.out.println("punya array belakang? " + langsung.hasArray());

        // Memori direct ditulis sebagai blok byte mentah; endianness
        // harus dinyatakan eksplisit agar sama dengan pihak native.
        ByteBuffer tampilan = langsung.order(ByteOrder.LITTLE_ENDIAN);
        tampilan.putInt(0, 0x0A0B0C0D);
        System.out.println("byte 0 sampai 3     : "
                + String.format("%02X %02X %02X %02X",
                        tampilan.get(0), tampilan.get(1),
                        tampilan.get(2), tampilan.get(3)));
    }
}