Tuning garbage collector adalah langkah terakhir optimasi, bukan pertama — baru masuk akal setelah tekanan alokasi sudah diturunkan dan profil menunjukkan GC memang penyumbang jeda. Bekerja di level ini tanpa angka terukur hanya menukar satu masalah dengan masalah lain. Chapter ini menyusun siklus tuning yang berbasis data: tiga masukan wajib dari pengukuran, kalkulator ukuran heap dan young gen atas angka pasti, peta keputusan collector menurut SLA, dan satu perbaikan arsitektural G1 terbaru — region pinning JEP 423.

Tiga Masukan yang Harus Terukur Dulu #

Setiap keputusan tuning membutuhkan tiga angka. Live set — byte objek yang benar-benar hidup setelah major GC — terbaca dari heap dump. Laju alokasi — byte per satuan waktu — terbaca dari profil alokasi JFR. Jeda nyata per koleksi terbaca dari log GC, yang di JDK modern diaktifkan dengan -Xlog:gc:

text Output nyata java -Xms256m -Xmx256m -Xlog:gc (durasi jeda berubah)
[0.002s][info][gc] Using G1
[0.547s][info][gc] GC(0) Pause Full (System.gc()) 81M->81M(256M) 6.417ms

Baris pertama memastikan collector aktif — G1 adalah bawaan JDK sejak versi 9. Format 81M->81M(256M) dibaca: heap sebelum koleksi, sesudah koleksi, dan kapasitas total. Sementara itu jcmd <pid> GC.heap_info pada proses uji dengan heap yang sama melaporkan region size 1024K — bukti G1 membagi heap menjadi region berukuran pangkat dua.

Menghitung Heap dan Young Gen #

Kalkulator berikut mengubah tiga masukan itu menjadi keputusan. Panduan umum yang dipakai: heap total sekitar tiga kali live set, dan young gen harus cukup besar agar total jeda minor GC per menit tetap di bawah anggaran SLA:

/**
 * Kalkulator tuning GC atas angka pasti: live set hasil heap dump, laju
 * alokasi hasil profil, dan jeda hasil -Xlog:gc. Seluruh keluaran adalah
 * matematika murni atas masukan tetap, bukan pengukuran runtime.
 */
public class KalkulatorTuning {

    record Kasus(String skenario, String sla, String rekomendasi) {}

    public static void main(String[] args) {
        // Masukan hasil pengukuran (angka tetap untuk contoh ini)
        int liveSetMb = 4096;               // heap dump setelah major GC
        int lajuAlokasiMbPerMenit = 12_000; // profil alokasi: 12 GB/menit
        int jedaPerMinorMs = 40;            // -Xlog:gc: rata-rata minor GC
        int anggaranGcPersen = 1;           // SLA: maksimal 1% waktu untuk GC

        // ---- 1. Ukuran heap total: panduan umum 3x live set ----
        int heapMb = 3 * liveSetMb;
        System.out.println("== Ukuran heap ==");
        System.out.println("live set        = " + liveSetMb + " MB");
        System.out.println("heap disarankan = 3 x live set = " + heapMb + " MB");
        System.out.printf("rasio live/heap = %.0f%%%n", 100.0 * liveSetMb / heapMb);

        // ---- 2. Young gen minimum dari anggaran jeda ----
        // (A / Y) * jeda <= anggaran% x 60.000 ms  =>  Y >= A * jeda / anggaran
        int yMin = (int) Math.ceil((double) lajuAlokasiMbPerMenit * jedaPerMinorMs
            / (anggaranGcPersen / 100.0 * 60_000));
        System.out.println();
        System.out.println("== Ukuran young gen ==");
        System.out.println("laju alokasi    = " + lajuAlokasiMbPerMenit + " MB/menit");
        System.out.println("jeda per minor  = " + jedaPerMinorMs + " ms");
        System.out.println("anggaran GC     = " + anggaranGcPersen + "% x 60.000 ms = "
            + (anggaranGcPersen / 100.0 * 60_000) + " ms/menit");
        System.out.println("young gen min   = " + yMin + " MB");

        // ---- 3. Bandingkan kandidat young gen ----
        System.out.println();
        System.out.println("== Kandidat young gen (jeda total per menit) ==");
        System.out.printf("%8s %10s %14s %11s%n", "Y (MB)", "GC/menit", "jeda/menit", "% waktu GC");
        for (int yMb : new int[] {512, 1024, 2048, 3072}) {
            double gcPerMenit = (double) lajuAlokasiMbPerMenit / yMb;
            double jedaPerMenit = gcPerMenit * jedaPerMinorMs;
            double persen = 100.0 * jedaPerMenit / 60_000;
            String status = yMb >= yMin ? "OK" : "terlalu kecil";
            System.out.printf("%8d %10.1f %9.0f ms %10.2f%%  %s%n",
                yMb, gcPerMenit, jedaPerMenit, persen, status);
        }

        // ---- 4. Region G1 ----
        System.out.println();
        System.out.println("== Region G1 (ukuran dibulatkan ke pangkat dua) ==");
        System.out.println("heap 12.288 MB / region 4 MB  = " + (12_288 / 4) + " region");
        System.out.println("heap    256 MB / region 1 MB  = " + (256 / 1) + " region"
            + "  <- cocok dengan output jcmd GC.heap_info mesin uji");

        // ---- 5. Pilihan collector dari SLA ----
        System.out.println();
        System.out.println("== Peta keputusan collector ==");
        System.out.printf("%-28s %-34s %s%n", "skenario", "SLA", "collector");
        for (Kasus k : new Kasus[] {
                new Kasus("ETL malam tanpa pengguna", "throughput maksimal", "-XX:+UseParallelGC"),
                new Kasus("Web API umum", "p99 di bawah 200 ms", "-XX:+UseG1GC (bawaan)"),
                new Kasus("Trading, heap 100 GB", "p99 di bawah 20 ms", "-XX:+UseZGC")}) {
            System.out.printf("%-28s %-34s %s%n", k.skenario(), k.sla(), k.rekomendasi());
        }
        System.out.println();
        System.out.println("Urutan kerja: ukur (heap dump, profil alokasi, -Xlog:gc),");
        System.out.println("hitung (kalkulator ini), lalu ubah satu variabel pada satu waktu.");
    }
}

Output yang Dihasilkan:

text
== Ukuran heap ==
live set        = 4096 MB
heap disarankan = 3 x live set = 12288 MB
rasio live/heap = 33%

== Ukuran young gen ==
laju alokasi    = 12000 MB/menit
jeda per minor  = 40 ms
anggaran GC     = 1% x 60.000 ms = 600.0 ms/menit
young gen min   = 800 MB

== Kandidat young gen (jeda total per menit) ==
  Y (MB)   GC/menit     jeda/menit  % waktu GC
     512       23.4       938 ms       1.56%  terlalu kecil
    1024       11.7       469 ms       0.78%  OK
    2048        5.9       234 ms       0.39%  OK
    3072        3.9       156 ms       0.26%  OK

== Region G1 (ukuran dibulatkan ke pangkat dua) ==
heap 12.288 MB / region 4 MB  = 3072 region
heap    256 MB / region 1 MB  = 256 region  <- cocok dengan output jcmd GC.heap_info mesin uji

== Peta keputusan collector ==
skenario                     SLA                                collector
ETL malam tanpa pengguna     throughput maksimal                -XX:+UseParallelGC
Web API umum                 p99 di bawah 200 ms                -XX:+UseG1GC (bawaan)
Trading, heap 100 GB         p99 di bawah 20 ms                 -XX:+UseZGC

Urutan kerja: ukur (heap dump, profil alokasi, -Xlog:gc),
hitung (kalkulator ini), lalu ubah satu variabel pada satu waktu.

Mari kita telaah bagian-bagian kuncinya:

  1. Baris 17-22 (heap total): dengan live set 4 GB, panduan tiga kali live set menghasilkan heap 12 GB dan rasio live/heap 33% — ruang gerak cukup agar koleksi tidak beruntun dan pengguna tidak menabrak batas atas.
  2. Baris 26-27 (yMin): turunan langsung dari pertidaksamaan (A / Y) × jeda ≤ anggaran. Laju alokasi 12 GB/menit dengan jeda 40 ms dan anggaran 1% menuntut young gen minimal 800 MB — angka yang mustahil ditebak tanpa rumus.
  3. Baris 40-47 (tabel kandidat): young gen 512 MB menghasilkan 1,56% waktu GC — melanggar SLA; 1 GB sudah memadai dengan 0,78%. Memperbesar young gen menurunkan frekuensi GC tetapi menaikkan durasi tiap koleksi — trade-off yang harus diukur ulang, bukan diasumsikan.
  4. Baris 49-54 (region): heap 12 GB dengan region 4 MB menghasilkan 3.072 region. Baris kedua sengaja menautkan ke kenyataan: heap 256 MB pada mesin uji memang menghasilkan region 1 MB, persis keluaran GC.heap_info yang kita kutip di atas.

Memilih Collector dari SLA #

Peta keputusan pada bagian akhir output merangkum filosofi pemilihan: Parallel memaksimalkan throughput dengan jeda panjang yang tidak masalah untuk job batch; G1 menyeimbangkan jeda dan throughput untuk layanan umum; ZGC menjanjikan jeda sub-milidetik dengan biaya memori lebih besar. Perbandingan mendalam keempat collector — termasuk Serial dan karakteristik masing-masing — sudah dibedah pada Chapter Jenis Garbage Collector di section JVM dan Memory Management, dan daftar lengkap flag-nya pada Chapter JVM Options.

Region Pinning di G1 #

Satu perbaikan arsitektural terbaru patut diketahui: JEP 423 (JDK 21) membawa region pinning ke G1. Sebelumnya, kode native lewat JNI critical region — misalnya GetPrimitiveArrayCritical — memaksa G1 melakukan koleksi penuh atau bahkan menonaktifkan koleksi concurrent selama area itu dipakai. Dengan region pinning, G1 cukup “menyemat” region yang berisi array bersangkutan dan tetap memungut region lain. Pelajarannya untuk kita sebagai penulis aplikasi: API critical JNI itu mahal versi lama, dan pada JVM modern pun sebaiknya dipakai sesempit mungkin.

Kalkulator dan peta keputusan menuntaskan sisi JVM; sisa besar performa aplikasi ditentukan di sisi kode. Chapter berikutnya pindah ke struktur data: memilih collection dan kapasitas awal yang tepat untuk pola akses kita.

Referensi #

Berkas Sumber

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

KalkulatorTuning.java

Unduh berkas
java
/**
 * Kalkulator tuning GC atas angka pasti: live set hasil heap dump, laju
 * alokasi hasil profil, dan jeda hasil -Xlog:gc. Seluruh keluaran adalah
 * matematika murni atas masukan tetap, bukan pengukuran runtime.
 */
public class KalkulatorTuning {

    record Kasus(String skenario, String sla, String rekomendasi) {}

    public static void main(String[] args) {
        // Masukan hasil pengukuran (angka tetap untuk contoh ini)
        int liveSetMb = 4096;               // heap dump setelah major GC
        int lajuAlokasiMbPerMenit = 12_000; // profil alokasi: 12 GB/menit
        int jedaPerMinorMs = 40;            // -Xlog:gc: rata-rata minor GC
        int anggaranGcPersen = 1;           // SLA: maksimal 1% waktu untuk GC

        // ---- 1. Ukuran heap total: panduan umum 3x live set ----
        int heapMb = 3 * liveSetMb;
        System.out.println("== Ukuran heap ==");
        System.out.println("live set        = " + liveSetMb + " MB");
        System.out.println("heap disarankan = 3 x live set = " + heapMb + " MB");
        System.out.printf("rasio live/heap = %.0f%%%n", 100.0 * liveSetMb / heapMb);

        // ---- 2. Young gen minimum dari anggaran jeda ----
        // (A / Y) * jeda <= anggaran% x 60.000 ms  =>  Y >= A * jeda / anggaran
        int yMin = (int) Math.ceil((double) lajuAlokasiMbPerMenit * jedaPerMinorMs
            / (anggaranGcPersen / 100.0 * 60_000));
        System.out.println();
        System.out.println("== Ukuran young gen ==");
        System.out.println("laju alokasi    = " + lajuAlokasiMbPerMenit + " MB/menit");
        System.out.println("jeda per minor  = " + jedaPerMinorMs + " ms");
        System.out.println("anggaran GC     = " + anggaranGcPersen + "% x 60.000 ms = "
            + (anggaranGcPersen / 100.0 * 60_000) + " ms/menit");
        System.out.println("young gen min   = " + yMin + " MB");

        // ---- 3. Bandingkan kandidat young gen ----
        System.out.println();
        System.out.println("== Kandidat young gen (jeda total per menit) ==");
        System.out.printf("%8s %10s %14s %11s%n", "Y (MB)", "GC/menit", "jeda/menit", "% waktu GC");
        for (int yMb : new int[] {512, 1024, 2048, 3072}) {
            double gcPerMenit = (double) lajuAlokasiMbPerMenit / yMb;
            double jedaPerMenit = gcPerMenit * jedaPerMinorMs;
            double persen = 100.0 * jedaPerMenit / 60_000;
            String status = yMb >= yMin ? "OK" : "terlalu kecil";
            System.out.printf("%8d %10.1f %9.0f ms %10.2f%%  %s%n",
                yMb, gcPerMenit, jedaPerMenit, persen, status);
        }

        // ---- 4. Region G1 ----
        System.out.println();
        System.out.println("== Region G1 (ukuran dibulatkan ke pangkat dua) ==");
        System.out.println("heap 12.288 MB / region 4 MB  = " + (12_288 / 4) + " region");
        System.out.println("heap    256 MB / region 1 MB  = " + (256 / 1) + " region"
            + "  <- cocok dengan output jcmd GC.heap_info mesin uji");

        // ---- 5. Pilihan collector dari SLA ----
        System.out.println();
        System.out.println("== Peta keputusan collector ==");
        System.out.printf("%-28s %-34s %s%n", "skenario", "SLA", "collector");
        for (Kasus k : new Kasus[] {
                new Kasus("ETL malam tanpa pengguna", "throughput maksimal", "-XX:+UseParallelGC"),
                new Kasus("Web API umum", "p99 di bawah 200 ms", "-XX:+UseG1GC (bawaan)"),
                new Kasus("Trading, heap 100 GB", "p99 di bawah 20 ms", "-XX:+UseZGC")}) {
            System.out.printf("%-28s %-34s %s%n", k.skenario(), k.sla(), k.rekomendasi());
        }
        System.out.println();
        System.out.println("Urutan kerja: ukur (heap dump, profil alokasi, -Xlog:gc),");
        System.out.println("hitung (kalkulator ini), lalu ubah satu variabel pada satu waktu.");
    }
}