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:

SumberAlamatKapan dibuka
GC Tuning Guide (JDK 25)docs.oracle.com/en/java/javase/25/gctuning/penyetelan collector dan ergonomi
Manual javadocs.oracle.com/en/java/javase/25/docs/specs/man/java.htmlarti bendera JVM seperti -XX:+UseZGC
API jdk.jfrdocs.oracle.com/en/java/javase/25/docs/api/java.base/jdk/jfr/package-summary.htmlperekaman peristiwa dari kode
JMHgithub.com/openjdk/jmhkerangka benchmark mikro
JMH Samplesgithub.com/openjdk/jmh/tree/master/jmh-samplespola 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.

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

text
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 kali

Mari kita telaah cara kerjanya:

  1. 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.
  2. 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.
  3. Baris 31-32 (promosi survivor): tiap siklus, survivor naik ke generasi tua. Kolom jedaGenerasional stabil di 14 sementara jedaSeluruhHeap tumbuh dari 54 ke 114 — inilah alasan desain generasional ada.
  4. 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.

java Benchmark JMH: dua cara menyusun string (konsep)
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 #

Berkas Sumber

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

ModelJedaGC.java

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