Chapter sebelumnya menetapkan konsep pemungutan: akar, penandaan, jeda stop-the-world. Namun “garbage collector” bukan satu implementasi — HotSpot membawa empat collector pilihan yang mengimplementasikan konsep itu dengan kompromi berbeda: ada yang memaksimalkan throughput dengan jeda panjang, ada yang mengejar jeda sub-milidetik dengan jejak memori lebih besar. JDK 25 mengaktifkan G1 secara bawaan, tetapi pilihan lain tetap relevan untuk beban kerja tertentu. Pada chapter ini kita bandingkan keempatnya, mendeteksi collector aktif lewat program, lalu menetapkan kriteria pemilihan berdasar karakter aplikasi.

Empat Kolektor Bawaan JDK #

Tabel berikut merangkum keempat collector yang tersedia di HotSpot JDK 25 beserta profil khasnya:

CollectorBendera aktifasiJeda khasThroughputJejak memoriPasangan alami
Serial-XX:+UseSerialGCpanjang, satu threadmoderatpaling kecilcontainer mini, mesin 1 CPU
Parallel-XX:+UseParallelGCpanjang, banyak threadtertinggisedangbatch, pemrosesan data
G1-XX:+UseG1GCpendek dan terkendalitinggisedangserver umum (bawaan)
ZGC-XX:+UseZGCsub-milidetiktinggibesarlayanan latensi rendah

Serial menjalankan seluruh fase GC dengan satu thread — jeda terpanjang, tetapi tanpa overhead koordinasi antar thread dan struktur data pendukung, sehingga paling hemat memori. Parallel memperkerjakan banyak thread untuk fase mark dan compact sehingga total waktu GC memendek; total jeda tetap ada dan panjang, namun waktu produktif aplikasi (throughput) paling tinggi. Keduanya memakai model generasional klasik yang akan kita telusuri di Chapter Object Lifecycle di JVM.

Melihat Kolektor Aktif #

Setiap collector mengelola heap dengan nama pool yang khas — G1 memakai G1 Eden Space, Parallel memakai awalan PS, Serial memakai nama generasi klasik, ZGC menampilkan pool generasinya sendiri. Nama pool bisa dibaca lewat API java.lang.management, dan polanya cukup khas untuk mengidentifikasi collector:

import java.lang.management.ManagementFactory;
import java.util.List;

public class DeteksiKolektor {
    public static void main(String[] args) {
        List<String> pool = ManagementFactory.getMemoryPoolMXBeans().stream()
                .map(b -> b.getName())
                .filter(n -> n.contains("Eden") || n.contains("Old")
                        || n.contains("Tenured") || n.contains("Young"))
                .sorted()
                .toList();

        String kolektor;
        if (pool.stream().anyMatch(n -> n.startsWith("G1"))) {
            kolektor = "G1";
        } else if (pool.stream().anyMatch(n -> n.startsWith("PS"))) {
            kolektor = "Parallel";
        } else if (pool.stream().anyMatch(n -> n.startsWith("ZGC"))) {
            kolektor = "ZGC";
        } else if (pool.contains("Tenured Gen")) {
            kolektor = "Serial";
        } else {
            kolektor = "tidak dikenali";
        }

        System.out.println("kolektor aktif : " + kolektor);
        pool.forEach(n -> System.out.println("  pool: " + n));
    }
}

Output yang Dihasilkan:

text
kolektor aktif : G1
  pool: G1 Eden Space
  pool: G1 Old Gen

Program ini mendeteksi G1 pada JVM tanpa bendera apa pun — bukti bahwa G1 adalah collector bawaan JDK 25. Jalankan ulang dengan bendera collector lain dan hasilnya berubah seketika:

bash Menjalankan dengan collector lain
java -XX:+UseSerialGC   DeteksiKolektor.java
java -XX:+UseParallelGC DeteksiKolektor.java
java -XX:+UseZGC        DeteksiKolektor.java
text Keluaran untuk -XX:+UseSerialGC (mesin uji)
kolektor aktif : Serial
  pool: Eden Space
  pool: Tenured Gen
text Keluaran untuk -XX:+UseZGC (mesin uji)
kolektor aktif : ZGC
  pool: ZGC Old Generation
  pool: ZGC Young Generation

Mari kita telaah cara kerja deteksinya:

  1. Baris 9-11 (filter stream): dari seluruh pool JVM, program hanya menyimpan pool generasional — kata Eden, Old, Tenured, atau Young di namanya. Pool Metaspace dan CodeHeap tersaring karena tidak relevan untuk identifikasi collector.
  2. Baris 14-24 (rantai if): pemeriksaan berurutan berdasarkan awalan khas. Awalan G1 hanya dipakai collector G1; PS adalah tanda Parallel Scavenge; ZGC milik ZGC; sementara kombinasi Eden Space dan Tenured Gen tanpa awalan adalah ciri Serial.
  3. Urutan pemeriksaan penting: ZGC Old Generation mengandung kata Old, tetapi tidak tertukar dengan G1 karena pemeriksaan startsWith("G1") dilakukan lebih dulu terhadap seluruh nama pool.

Karakter G1 dan ZGC #

G1 (Garbage First) membagi heap menjadi ribuan region berukuran sama (1-32 MB, tergantung ukuran heap) alih-alih tiga generasi runtut. Region mana pun dapat berperan sebagai Eden, Survivor, atau Old, dan region Old yang paling banyak berisi sampah dikumpulkan lebih dulu — dari sini nama garbage first berasal. Penandaan hidup berjalan concurrent bersama aplikasi, disusul evakuasi singkat yang stop-the-world; hasilnya jeda khas di bawah puluhan milidetik dan target jeda yang bisa diatur lewat -XX:MaxGCPauseMillis.

ZGC (Z Garbage Collector) dirancang agar jeda tidak lagi skala dengan ukuran heap — pemindahan objek terjadi sambil aplikasi berjalan, dimungkinkan oleh colored pointers dan load barriers di level VM. Sejak JEP 474, ZGC berjalan mode generational: objek muda dan tua dipisahkan agar objek berumur pendek — mayoritas sampah di aplikasi khas — dikumpulkan dengan upaya jauh lebih kecil. Jeda khasnya di bawah satu milidetik bahkan pada heap puluhan GB. Bayarannya ada pada jejak memori lebih besar untuk metadata dan sedikit CPU untuk load barrier.

Memilih Berdasar Beban Kerja #

Kerangka memilihnya sederhana. Ukur dulu SLA aplikasi: berapa milidetik jeda yang masih bisa diterima klien, dan berapa persen waktu CPU yang boleh habis untuk GC. Bila jeda panjang tidak berarti — laporan malam, ETL, komputasi ilmiah — Parallel memberi throughput terbaik. Bila jeda puluhan milidetik cukup dan heap berukuran sedang (hingga belasan GB), G1 seimbang di kedua sisi. Bila setiap milidetik dihitung atau heap puluhan GB, ZGC layak diuji dengan membayar memori ekstra. Serial tetap berharga di container satu CPU tempat overhead thread GC justru memperburuk keadaan. Setelah collector dipilih, barulah ukuran heap dan generasi disetel — alur kerja lengkapnya kita rangkum pada Chapter Memory Tuning di akhir Section ini.

Referensi #

Berkas Sumber

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

DeteksiKolektor.java

Unduh berkas
java
import java.lang.management.ManagementFactory;
import java.util.List;

public class DeteksiKolektor {
    public static void main(String[] args) {
        List<String> pool = ManagementFactory.getMemoryPoolMXBeans().stream()
                .map(b -> b.getName())
                .filter(n -> n.contains("Eden") || n.contains("Old")
                        || n.contains("Tenured") || n.contains("Young"))
                .sorted()
                .toList();

        String kolektor;
        if (pool.stream().anyMatch(n -> n.startsWith("G1"))) {
            kolektor = "G1";
        } else if (pool.stream().anyMatch(n -> n.startsWith("PS"))) {
            kolektor = "Parallel";
        } else if (pool.stream().anyMatch(n -> n.startsWith("ZGC"))) {
            kolektor = "ZGC";
        } else if (pool.contains("Tenured Gen")) {
            kolektor = "Serial";
        } else {
            kolektor = "tidak dikenali";
        }

        System.out.println("kolektor aktif : " + kolektor);
        pool.forEach(n -> System.out.println("  pool: " + n));
    }
}