Ringkasan Eksekutif

Saya baru baca tulisan Inside Java soal perubahan performa di JDK 28 untuk Foreign Function & Memory (FFM) API: alokasi memory off-heap lewat confined arena sekarang dipooling di level native, dan itu bikin alokasi kecil jadi 6-18x lebih cepat tanpa developer perlu mengubah satu baris kode pun. Yang menarik buat saya, ini optimasi murni di bawah permukaan — kontrak lifetime, zero-initialization, dan thread-confinement dari Arena.ofConfined() tetap persis sama seperti sebelumnya.

Masalah yang Diselesaikan

Pola yang paling sering dipakai adalah confined arena sebagai scratch space sekitar satu pemanggilan native, misalnya buat nampung hasil errno atau sekadar satu pointer/int/long. Sebelum JDK 28, bahkan alokasi segitu kecil tetap lewat jalur native allocator penuh plus registrasi cleanup action, dan menurut artikelnya, overhead bookkeeping ini kadang lebih mahal daripada akses ke memory-nya sendiri. Data instrumentasi test-suite OpenJDK selama pengembangan fitur ini menunjukkan lebih dari 99.99% confined arena yang benar-benar mengalokasikan memory ternyata memakai kurang dari 64 byte — jadi kasus "alokasi mini, dominasi overhead" ini bukan kasus langka, tapi mayoritas.

Fitur Utama dan Contoh Kode

Mekanismenya: tiap platform thread secara lazy memelihara cache kecil berisi pool native-memory, default-nya 4 pool berukuran 64 byte masing-masing. Arena baru gak langsung ambil pool begitu dibuat — pool baru diambil saat arena menerima alokasi pertama yang memang bisa dilayani dari pool. Begitu dapat, pool itu jadi milik eksklusif arena tersebut, dan alokasi berikutnya di-carve berurutan dari sisa kapasitas pool. Kalau ukurannya gak fit atau butuh alignment yang gak bisa dijamin pool, alokasi otomatis fallback ke jalur native allocator biasa — jadi ini sifatnya opportunistic, bukan menggantikan jalur lama.

Contoh pola umum yang disorot di artikel (pattern ini sekarang otomatis lebih cepat tanpa perlu diubah):

try (Arena arena = Arena.ofConfined()) {
    MemorySegment result = arena.allocate(ValueLayout.JAVA_INT);
    nativeFunction.invokeExact(result); // menulis hasil ke segment yang disediakan
    return result.get(ValueLayout.JAVA_INT, 0);
}

Saat arena close, bagian pool yang terpakai di-zero lalu dikembalikan ke cache thread tersebut, atau dilepas ke native allocator kalau cache sudah penuh (4 pool). Untuk virtual thread, desainnya beda: menambah cache native-memory ke tiap virtual thread akan merusak footprint ringannya, jadi virtual thread malah "numpang" pool dari carrier thread-nya saat alokasi pertama. Ada pinning singkat pas proses handoff itu, tapi begitu pool berpindah kepemilikan ke arena, virtual thread tetap bisa migrasi antar carrier dengan aman selama arena masih terbuka. Saat arena itu ditutup, pool dikembalikan ke cache carrier mana pun yang sedang dipakai saat itu.

Semua garansi safety lama tetap dipertahankan: begitu arena close, scope-nya langsung gak alive dan segment-nya langsung gak bisa diakses. Sebelum pool dipakai ulang oleh arena lain, isinya di-zero dulu — jadi gak ada kebocoran data antar-arena lewat pool yang sama.

Dampak dan Catatan Upgrade

Nomor performanya cukup besar. Untuk alokasi 5 byte dan 20 byte, speedup nominal (JMH average-time baseline dibagi latency versi pooled) yang dilaporkan: Linux AArch64 12.3x/8.6x, Linux x64 6.8x/7.8x, macOS AArch64 18.6x/16.8x, dan Windows x64 16.8x/18.9x. Di benchmark tambahan pada Apple M4, alokasi confined 5 byte turun dari sekitar 15.9 ns/op (tanpa pool) jadi cuma 1.05 ns/op (dengan pool). Tapi begitu ukuran alokasi lewat 64 byte (default kapasitas pool), angkanya balik ke jalur biasa dan nggak ada speedup lagi — jadi ini optimasi yang spesifik buat alokasi kecil. Virtual thread juga kebagian untung meski ada overhead tambahan dari carrier handoff, speedup-nya tetap signifikan dibanding non-pooled.

Satu hal yang perlu dicatat buat yang pegang tooling diagnostik: closing confined arena yang pooled sekarang bisa cuma mengembalikan memory ke cache JDK, bukan langsung memanggil native free. Jadi asumsi 1:1 antara alokasi arena dan pemanggilan malloc/free di level native sudah gak berlaku lagi — kalau ada instrumentation atau memory profiler yang mengandalkan asumsi itu, perlu disesuaikan.

Yang paling diuntungkan: binding FFM yang bikin confined arena per pemanggilan native, wrapper hasil generate jextract, native handle/pointer out-parameter, string atau byte array pendek yang dikirim ke native code, dan kode latency-sensitive yang sering banget manggil native function kecil-kecil. Perlu diingat juga ini cuma berlaku buat confined arena (Arena.ofConfined()) — arena shared, global, atau auto gak kebagian optimasi ini. Kalau mau lihat detail teknis lebih jauh, artikelnya mengarah ke JDK-8385697, OpenJDK PR #31365, dan CSR JDK-8390770.

---

Pertama diumumkan di Inside Java. Baca sumber aslinya untuk detail lengkap.