Bab-bab sebelumnya memberi kita peta lengkap memori JVM: heap dengan generasinya, Metaspace, alat pemantauan, dan pilihan collector. Penutup Section ini merangkai semuanya menjadi proses tuning yang tertib. Penyetelan memori yang berhasil jarang berupa jalan pintas rahasia; ia selalu berupa siklus ukur–analisis–ubah–ukur ulang, karena angka optimal satu aplikasi tidak berlaku bagi aplikasi lain. Yang bisa dibakukan adalah urutan kerjanya, dan itulah yang akan kita susun di sini.
Langkah Nol: Ukur Sebelum Menyetel #
Tuning tanpa pengukuran bukan tuning melainkan menebak tanpa data. Titik awal yang benar selalu data: catat durasi jeda GC (jstat -gcutil atau log -Xlog:gc), identifikasi collector yang aktif, dan periksa apakah gejala sesungguhnya adalah tekanan memori. Gejala klasik yang menunjukkan heap kurang besar: frekuensi full GC meningkat, waktu pause memanjang, dan utilisasi old generation menetap tinggi. Sebaliknya, bila GC sehat tetapi CPU tinggi, masalahnya biasanya bukan heap — melainkan alokasi berlebihan di kode. Program kecil berikut memperlihatkan beban alokasi yang sehat, yang penyetelannya berhenti sebelum dimulai:
import java.util.ArrayList;
import java.util.List;
public class AlokasiUji {
// Payload tetap agar perbandingan antar konfigurasi adil
record Transaksi(long id, int jumlah) {
}
public static void main(String[] args) {
int total = 200_000;
List<Transaksi> tersimpan = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
tersimpan.add(new Transaksi(i + 1, 10_000 + (i % 90) * 100));
}
long jumlahRecord = tersimpan.stream().filter(t -> t.jumlah() >= 10_500).count();
System.out.println("transaksi tersimpan : " + tersimpan.size());
System.out.println("transaksi disaring : " + jumlahRecord);
System.out.println("pola muat : "
+ (tersimpan.size() == total ? "utuh, tanpa tekanan GC" : "terpotong"));
}
}Output yang Dihasilkan:
transaksi tersimpan : 200000
transaksi disaring : 188885
pola muat : utuh, tanpa tekanan GCDua ratus ribu record singkat hidup di Eden, dipungut minor GC berturut-turut, dan hanya sebagian kecil yang berumur panjang di list — profil alokasi ideal yang tidak menuntut penyetelan heap apa pun. Kesimpulan “tidak perlu disetel” adalah hasil tuning yang sah, dan sering kali yang paling murah.
Verifikasi Konfigurasi Berjalan #
Sebelum mengubah flag, pastikan kita tahu konfigurasi yang sedang berjalan. Management API memperlihatkan collector aktif dan batas heap dalam proses itu sendiri. Hasil berikut diambil dari JVM yang sengaja dinyalakan dengan flag berbeda-beda (nilai heap Anda akan berbeda sesuai flag yang dipakai):
java -Xms32m -Xmx64m -XX:+UseSerialGC Demo.javaOutput yang Dihasilkan:
GC : [Copy, MarkSweepCompact]
heap : 61 MiB (dari -Xmx64m)java -Xmx256m -XX:+UseZGC Demo.javaOutput yang Dihasilkan:
GC : [ZGC Minor Cycles, ZGC Minor Pauses, ZGC Major Cycles, ZGC Major Pauses]
heap : 256 MiBTanpa flag pilihan, JDK 25 memulai dengan G1 ([G1 Young Generation, G1 Concurrent GC, G1 Old Generation] pada mesin uji). Tiga profil ini adalah pilihan praktis penyetel: Serial untuk wadah kecil satu CPU, G1 sebagai penyeimbang umum, dan ZGC ketika anggaran jeda di bawah beberapa milidetik bersifat kontrak.
Peta Keputusan Penyetelan #
Dengan data di tangan, keputusan mengikuti pola yang dapat digambar sebagai pohon. Pertama, tentukan tujuan yang terukur: latensi p99 di bawah X milidetik, atau throughput di atas Y persen — tanpa angka target, tidak ada cara menilai berhasil. Kedua, baca gejalanya: full GC sering dengan old generation penuh berarti data berumur panjang melebihi kapasitas — naikkan -Xmx atau periksa kebocoran (Chapter Memory Leaks) sebelum menambah memori; jeda minor GC yang panjang pada heap raksasa berarti generasi muda terlalu besar — kecilkan -Xmn atau biarkan G1 menyesuaikan otomatis. Ketiga, di lingkungan container, gunakan -XX:MaxRAMPercentage alih-alih -Xmx mutlak agar heap menyesuaikan batas wadah, dan tinggalkan ruang bagi memori off-heap serta Metaspace — kesalahan klasik container adalah -Xmx sama dengan limit memori pod, berujung OOM kill oleh kernel.
Kapan Berhenti #
Penyetelan yang sehat memiliki titik berhenti. Bila target latensi tercapai dan GC hanya menyita porsi kecil CPU, berhentilah — kompleksitas konfigurasi juga biaya perawatan. Bila setelah dua-tiga iterasi gejala bertahan, arahkan perhatian ke kode: pola alokasi di hot path (Stream berantai yang membuat jutaan objek pernahkasus, cache tanpa eviksi, DTO besar disalin berulang) sering memberi kenaikan lebih besar daripada flag apa pun. Profil alokasi lewat JFR (jcmd <pid> JFR.start lalu buka di JDK Mission Control) menunjukkan persis constructor mana yang paling sering dipanggil. Flag menggeser batas; desain kode menggeser kebutuhannya.
Peta memori JVM kini tuntas — dari arsitektur hingga penyetelan. Section berikutnya berpindah ke alat yang membangun perangkat lunak itu sendiri: build tools dan dependency management.
Referensi #
- G1 Garbage Collector Tuning Guide (Oracle)
- JEP 474: ZGC Generational Mode
- jcmd: JFR.start dan perekaman profil
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AlokasiUji.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class AlokasiUji {
// Payload tetap agar perbandingan antar konfigurasi adil
record Transaksi(long id, int jumlah) {
}
public static void main(String[] args) {
int total = 200_000;
List<Transaksi> tersimpan = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
tersimpan.add(new Transaksi(i + 1, 10_000 + (i % 90) * 100));
}
long jumlahRecord = tersimpan.stream().filter(t -> t.jumlah() >= 10_500).count();
System.out.println("transaksi tersimpan : " + tersimpan.size());
System.out.println("transaksi disaring : " + jumlahRecord);
System.out.println("pola muat : "
+ (tersimpan.size() == total ? "utuh, tanpa tekanan GC" : "terpotong"));
}
}
KalkulatorTuning.java
Unduh berkaspublic class KalkulatorTuning {
public static void main(String[] args) {
long ramMb = Long.parseLong(args[0]);
double persenHeap = args.length > 1 ? Double.parseDouble(args[1]) : 75.0;
long heapMb = Math.round(ramMb * persenHeap / 100.0);
long youngMb = Math.round(heapMb / 3.0);
long oldMb = heapMb - youngMb;
System.out.println("RAM container : " + ramMb + " MB");
System.out.println("heap " + persenHeap + "% RAM : " + heapMb + " MB (flag: -XX:MaxRAMPercentage=" + persenHeap + ")");
System.out.println("young ~1/3 heap : " + youngMb + " MB");
System.out.println("old ~2/3 heap : " + oldMb + " MB");
}
}