Pertanyaan performa di Java jarang berdiri sendiri: aplikasi melambat, latensi naik, CPU habis — dan jawabannya bisa berada di garbage collector, di kode, atau di pola akses data. Untungnya JVM adalah salah satu runtime dengan instrumen diagnosa paling lengkap, dan semuanya terdokumentasi resmi: panduan penyetelan GC, halaman manual tiap tool, API perekaman peristiwa, hingga kerangka benchmark JMH. Chapter ini memetakan sumber tersebut dan memperagakan cara membacanya lewat satu model biaya jeda GC.
Peta Sumber Performa #
Tabel berikut merangkum sumber yang kita buka saat menyelidiki performa aplikasi Java:
| Sumber | Alamat | Kapan dibuka |
|---|---|---|
| GC Tuning Guide (JDK 25) | docs.oracle.com/en/java/javase/25/gctuning/ | penyetelan collector dan ergonomi |
| Manual java | docs.oracle.com/en/java/javase/25/docs/specs/man/java.html | arti bendera JVM seperti -XX:+UseZGC |
| API jdk.jfr | docs.oracle.com/en/java/javase/25/docs/api/java.base/jdk/jfr/package-summary.html | perekaman peristiwa dari kode |
| JMH | github.com/openjdk/jmh | kerangka benchmark mikro |
| JMH Samples | github.com/openjdk/jmh/tree/master/jmh-samples | pola benchmark siap dipelajari |
Urutan membacanya mengikuti urutan menyelidiki: ukur dulu dengan profil (JFR), pahami komponen yang bersalah lewat dokumennya, baru buktikan dugaan dengan benchmark (JMH). Pengantar ringan tentang JVM juga tersedia di dev.java/learn/jvm/.
Membaca Dokumen GC Tuning #
GC Tuning Guide adalah dokumen yang paling sering dikutip saat membahas performa JVM. Strukturnya konsisten antar rilis: pengenalan konsep pemungutan, penjelasan tiap collector (Serial, Parallel, G1, ZGC) beserta benderanya, lalu bab ergonomi — cara JVM memilih ukuran heap secara bawaan. Cara memakainya: jangan mulai dari daftar bendera, tetapi dari profil aplikasi — tetapkan batas latensi yang dapat diterima klien, cocokkan dengan profil collector di dokumen, lalu coba satu-dua bendera saja dan ukur ulang.
Model Biaya untuk Berpikir tentang Jeda GC #
Sebelum menyentuh bendera, mari bangun intuisi dengan model biaya deterministik — teknik yang sama dengan proyek Pemrosesan Data Asinkron. Premisnya satu: koleksi yang menyalin hanya membayar objek yang masih hidup, bukan seluruh heap. Konsekuensinya, memisahkan objek muda dan tua membuat jeda koleksi muda tetap kecil sekalipun data tua menumpuk.
public class ModelJedaGC {
public static void main(String[] args) {
// Model biaya: satu unit = waktu memproses satu objek oleh GC.
int objekPerSiklus = 240; // objek sementara yang dialokasikan tiap siklus
int rasioBertahan = 5; // persen objek muda yang masih hidup saat koleksi
int objekTuaAwal = 40; // objek lama yang hidup stabil di generasi tua
int biayaTetap = 2; // unit tetap per kejadian koleksi
int jumlahSiklus = 6;
int objekTua = objekTuaAwal;
int totalGenerasional = 0;
int totalSeluruh = 0;
System.out.println("Model jeda GC: " + jumlahSiklus + " siklus, "
+ objekPerSiklus + " objek/siklus, " + rasioBertahan + "% bertahan");
System.out.println();
System.out.println("siklus objekTua jedaGenerasional jedaSeluruhHeap");
for (int siklus = 1; siklus <= jumlahSiklus; siklus++) {
int bertahan = objekPerSiklus * rasioBertahan / 100;
int jedaGenerasional = biayaTetap + bertahan;
int jedaSeluruhHeap = biayaTetap + bertahan + objekTua;
totalGenerasional += jedaGenerasional;
totalSeluruh += jedaSeluruhHeap;
System.out.printf("%4d%12d%16d%16d%n", siklus, objekTua,
jedaGenerasional, jedaSeluruhHeap);
// Survivor dipromosikan ke generasi tua pada siklus berikutnya.
objekTua += bertahan;
}
System.out.println();
System.out.println("Total jeda generasional : " + totalGenerasional + " unit");
System.out.println("Total jeda seluruh heap : " + totalSeluruh + " unit");
System.out.println("Perbandingan jeda : "
+ String.format("%.1f kali", totalSeluruh / (double) totalGenerasional));
}
}Output yang Dihasilkan:
Model jeda GC: 6 siklus, 240 objek/siklus, 5% bertahan
siklus objekTua jedaGenerasional jedaSeluruhHeap
1 40 14 54
2 52 14 66
3 64 14 78
4 76 14 90
5 88 14 102
6 100 14 114
Total jeda generasional : 84 unit
Total jeda seluruh heap : 504 unit
Perbandingan jeda : 6.0 kaliMari kita telaah cara kerjanya:
- Baris 5-9 (konstanta model): menetapkan skenario — 240 objek sementara per siklus, 5% masih hidup saat koleksi, 40 objek lama yang hidup stabil. Semua angka beku sehingga keluaran selalu identik.
- Baris 21-23 (dua rumus jeda): jeda generasional hanya membayar objek hidup di eden — 2 unit tetap ditambah 12 survivor. Koleksi seluruh heap membayar objek yang sama ditambah seluruh objek tua.
- Baris 31-32 (promosi survivor): tiap siklus, survivor naik ke generasi tua. Kolom
jedaGenerasionalstabil di 14 sementarajedaSeluruhHeaptumbuh dari 54 ke 114 — inilah alasan desain generasional ada. - Baris 38-39 (perbandingan akhir): total jeda seluruh heap enam kali lebih besar. Angka pastinya bergantung skenario, tetapi arah kesimpulannya sama: pisahkan yang berumur pendek dari yang berumur panjang.
Benchmark Mikro dengan JMH #
Model biaya melatih intuisi; bukti angka mutakhir datang dari benchmark. Godaan terbesar pemula adalah mengukur sendiri dengan for dan System.nanoTime() — hasilnya menyesatkan karena compiler JIT mengompilasi kode panas di tengah pengukuran, optimizer dapat membuang hasil yang tidak dipakai, dan gangguan sistem operasi ikut terukur. JMH (Java Microbenchmark Harness) dari proyek OpenJDK menangani ketiganya lewat siklus warm-up, Blackhole, dan proses @Fork terpisah.
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(1)
public class SusunString {
private int[] angka = {1, 2, 3, 4, 5, 6, 7, 8};
@Benchmark
public String penggabungan() {
String hasil = "";
for (int a : angka) {
hasil += a;
}
return hasil;
}
@Benchmark
public String stringBuilder() {
StringBuilder hasil = new StringBuilder();
for (int a : angka) {
hasil.append(a);
}
return hasil;
}
}Contoh terbaik untuk belajar JMH ada di direktori jmh-samples pada repositori yang sama: lebih dari tiga puluh contoh bernomor, masing-masing mendemonstrasikan satu jebakan pengukuran beserta penanganannya.
Dengan peta ini, jalur penyelidikan performa menjadi lurus: profil dengan JFR, pahami lewat dokumen, buktikan dengan JMH. Ada pula momen ketika aplikasi tidak sekadar lambat — aplikasi berhenti atau melempar exception yang tak dikenal. Chapter berikutnya menyiapkan resep diagnosis cepat.
Referensi #
- Oracle: Garbage Collection Tuning Guide (JDK 25)
- Oracle: Manual java (JDK 25)
- OpenJDK: JMH (Java Microbenchmark Harness)
- Oracle: API jdk.jfr (JDK 25)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ModelJedaGC.java
Unduh berkaspublic class ModelJedaGC {
public static void main(String[] args) {
// Model biaya: satu unit = waktu memproses satu objek oleh GC.
int objekPerSiklus = 240; // objek sementara yang dialokasikan tiap siklus
int rasioBertahan = 5; // persen objek muda yang masih hidup saat koleksi
int objekTuaAwal = 40; // objek lama yang hidup stabil di generasi tua
int biayaTetap = 2; // unit tetap per kejadian koleksi
int jumlahSiklus = 6;
int objekTua = objekTuaAwal;
int totalGenerasional = 0;
int totalSeluruh = 0;
System.out.println("Model jeda GC: " + jumlahSiklus + " siklus, "
+ objekPerSiklus + " objek/siklus, " + rasioBertahan + "% bertahan");
System.out.println();
System.out.println("siklus objekTua jedaGenerasional jedaSeluruhHeap");
for (int siklus = 1; siklus <= jumlahSiklus; siklus++) {
int bertahan = objekPerSiklus * rasioBertahan / 100;
int jedaGenerasional = biayaTetap + bertahan;
int jedaSeluruhHeap = biayaTetap + bertahan + objekTua;
totalGenerasional += jedaGenerasional;
totalSeluruh += jedaSeluruhHeap;
System.out.printf("%4d%12d%16d%16d%n", siklus, objekTua,
jedaGenerasional, jedaSeluruhHeap);
// Survivor dipromosikan ke generasi tua pada siklus berikutnya.
objekTua += bertahan;
}
System.out.println();
System.out.println("Total jeda generasional : " + totalGenerasional + " unit");
System.out.println("Total jeda seluruh heap : " + totalSeluruh + " unit");
System.out.println("Perbandingan jeda : "
+ String.format("%.1f kali", totalSeluruh / (double) totalGenerasional));
}
}