Tur Algoritma GC

Berkenalan dengan Serial, Parallel, G1, dan ZGC beserta karakter masing-masing.

Daftar isi

Kontrak garbage collection di JVM dapat dijalankan oleh beberapa implementasi algoritma yang berbeda, dan setiap implementasi disebut collector atau kolektor. Pilihan kolektor menentukan karakter jeda, kebutuhan thread, dan throughput aplikasi tanpa mengubah satu baris kode pun. Topik ini memperkenalkan Serial, Parallel, G1, dan ZGC, menunjukkan cara mengetahui kolektor yang sedang aktif, lalu menimbang kapan tiap pilihan masuk akal. Semua uji dijalankan pada JDK 21 yang secara default memakai G1.

Empat kolektor dalam satu tabel#

Tabel berikut merangkum karakter keempat kolektor yang paling sering ditemui pada JDK 21.

KolektorStrategi dasarKarakter jedaWajar dipakai untukFlag
Serialgenerasional, satu thread GCstop-the-world paling panjangprogram kecil dan container hemat memori-XX:+UseSerialGC
Parallelgenerasional, banyak thread GCstop-the-world, mengejar throughputpekerjaan batch dan komputasi latar-XX:+UseParallelGC
G1heap dibagi menjadi regionstop-the-world dengan target jeda yang bisa diaturdefault sejak JDK 9, aplikasi server umum-XX:+UseG1GC
ZGChampir seluruhnya konkurenjeda milidetik, nyaris tak bergantung ukuran heapheap besar dengan sasaran latensi rendah-XX:+UseZGC

Semua kolektor mengerjakan pekerjaan yang sama, tetapi tiap kolektor membagi pekerjaannya secara berbeda antara thread yang berjalan saat aplikasi berhenti dan thread yang berjalan di sampingnya. Kolektor generasional klasik seperti Serial dan Parallel menyelesaikan seluruh siklusnya saat aplikasi beku, sedangkan G1 menyela pekerjaannya demi mengejar target jeda, dan ZGC menyerahkan hampir seluruh pekerjaan ke thread konkuren (concurrent). Flag pada kolom terakhir mengaktifkan masing-masing pilihan hanya pada perintah java, tanpa menyentuh kode. Perbedaan karakternya baru terasa pada aplikasi yang berjalan lama dengan tingkat alokasi tinggi, sehingga pemilihan tanpa pengukuran hanyalah tebakan.

Mengetahui kolektor aktif dari dalam program#

JVM mengelola satu atau beberapa MXBean berjenis GarbageCollectorMXBean untuk setiap kolektor aktif, dan namanya dapat dibaca lewat API manajemen standar. Program berikut mencetak nama seluruh bean GC yang aktif.

java UjiKolektorAktif.java
import java.lang.management.ManagementFactory;

public class UjiKolektorAktif {

    public static void main(String[] args) {
        for (var bean : ManagementFactory.getGarbageCollectorMXBeans()) {
            System.out.println(bean.getName());
        }
    }
}
text Keluaran
G1 Young Generation
G1 Concurrent GC
G1 Old Generation

Tiga baris itu bukan tiga kolektor berbeda, melainkan satu kolektor G1 yang terlihat dari tiga fasenya, yaitu fase generasi muda, fase konkuren, dan fase generasi tua. Nama bean berubah sesuai kolektor yang aktif. Dengan flag -XX:+UseSerialGC, bean yang muncul bernama Copy dan MarkSweepCompact, dengan -XX:+UseParallelGC bernama PS Scavenge dan PS MarkSweep, sedangkan dengan -XX:+UseZGC bernama ZGC Cycles dan ZGC Pauses. Nama inilah yang biasanya terbaca pada alat pemantauan, sehingga mengenalinya membantu saat membaca grafik produksi.

Bean yang sama juga membawa penghitung bawaan, yaitu jumlah siklus GC dan total waktunya sejak JVM menyala. Dua angka itu cukup untuk memeriksa secara cepat apakah sebuah program menghabiskan umurnya di dalam GC, tanpa alat tambahan apa pun. Daftar bean dapat berubah antarversi JDK, sehingga program yang mengurainya sebaiknya menanggapi nama yang tidak dikenal, bukan menganggap daftarnya permanen.

Serial dan Parallel: generasional klasik#

Serial dan Parallel memakai cetak biru yang sama, yaitu heap generasional dengan seluruh pekerjaan GC berlangsung saat aplikasi berhenti. Perbedaannya ada pada jumlah thread pekerja. Serial memakai satu thread sehingga jejak memorinya paling kecil dan tingkah lakunya paling mudah diprediksi, tetapi jeda panjangnya tumbuh seiring ukuran heap. Karena itu Serial wajar dipakai pada program kecil atau container yang memorinya terbatas.

Parallel menyerahkan pekerjaan itu ke banyak thread sekaligus sehingga jeda menyusut drastis dibanding Serial pada heap yang sama. Sasarannya adalah throughput maksimal, yaitu total waktu aplikasi menggarap pekerjaan berguna, bukan jeda sesaat yang paling pendek. Ukuran tiap generasi disetel otomatis saat berjalan, sehingga alokasi memori berpindah mengikuti perilaku program tanpa campur tangan. Untuk pekerjaan batch, pemrosesan data, dan komputasi latar yang tidak berhadapan langsung dengan pengguna, karakter ini cocok. Sebaliknya, aplikasi interaktif akan merasakan setiap jeda full GC-nya sebagai kabut sesaat di antarmuka.

G1: kolektor bawaan sejak JDK 9#

G1 membagi heap menjadi ratusan region berukuran sama dan mengelolanya secara fleksibel, bukan lagi blok generasi yang rapat. Pekerjaan GC dikumpulkan dalam ronde kecil, dan G1 memilih region yang sampahnya paling banyak lebih dulu, kebiasaan yang menjadi asal nama Garbage-First. Target jeda dapat diatur lewat flag -XX:MaxGCPauseMillis, secara default 200 milidetik, dan G1 menyesuaikan banyaknya region yang dikerjakan tiap ronde agar target itu tercapai.

Karakter ini membuat G1 menjadi pilihan default yang seimbang untuk aplikasi umum, termasuk aplikasi server dengan heap beberapa gigabyte. Jeda yang muncul tetap stop-the-world, tetapi panjangnya terkendali karena tiap ronde hanya menggarap sebagian region. Ada satu pengecualian yang perlu diwaspadai, yaitu objek yang ukurannya melebihi setengah region. Objek semacam itu ditempatkan langsung di deretan region berurutan dan sering menjadi penyebab jeda G1 yang membengkak, sehingga array raksasa patut dicurigai lebih dulu ketika jeda G1 memburuk tanpa sebab yang jelas. Pada heap yang sangat besar dengan sasaran jeda milidetik, kemampuan G1 mulai terbatas, dan di sanalah kolektor konkuren berperan.

ZGC: jeda milidetik untuk heap raksasa#

ZGC menyerahkan hampir seluruh pekerjaan GC ke thread yang berjalan di samping aplikasi, sehingga jeda stop-the-world-nya tinggal milidetik dan nyaris tidak membesar meski heap membesar. Kuncinya ada dua mekanisme di tingkat VM, yaitu penandaan warna pada pointer (colored pointers) dan pembatas pemuatan (load barrier) yang memeriksa setiap referensi saat dibaca. Sejak JDK 21, ZGC menjadi generasional, sehingga pemrosesan objek berumur pendeknya juga mengikuti hipotesis generasional yang dibahas pada topik garbage collection.

Kelebihan itu membayar harga yang jelas. Pekerjaan konkuren merebut CPU dan memori dari aplikasi, sehingga throughput keseluruhan bisa turun dibanding kolektor yang menumpuk pekerjaannya saat aplikasi berhenti. Untuk heap belasan gigabyte dengan sasaran latensi rendah, harga itu sering sepadan, sedangkan untuk program kecil ZGC justru mubazir. Ciri yang paling menentukan pemilihannya adalah sifat jeda yang nyaris tidak membesar ketika heap diperbesar, sehingga menambah memori untuk menampung beban tidak lagi berarti memperburuk waktu respons.

Memilih kolektor pada baris perintah#

Kolektor diaktifkan lewat flag pada perintah java, dan program yang sama dapat dicoba pada masing-masing pilihan.

bash
# tidak-dijalankan
# satu perintah, satu kolektor yang berbeda
java -XX:+UseSerialGC UjiKolektorAktif.java
java -XX:+UseParallelGC UjiKolektorAktif.java
java -XX:+UseG1GC UjiKolektorAktif.java
java -XX:+UseZGC UjiKolektorAktif.java

Program yang sama akan mencetak nama bean yang berbeda pada tiap perintah, dan perilaku jeda yang berbeda pula. Nama kolektor yang dipilih ergonomi JVM juga terbaca pada baris pertama log -Xlog:gc yang diperkenalkan pada topik garbage collection, sehingga pilihan bawaan dapat dikonfirmasi tanpa alat tambahan. Pemilihan yang sehat berjalan dua arah, yaitu mengukur karakter beban kerja terlebih dahulu, mencoba kandidat kolektor, lalu membandingkan distribusi jeda yang terukur. Perbandingan hanya bermakna pada beban kerja yang sama, ukuran heap yang sama, dan beberapa kali pengulangan, karena satu kali jalan mudah teracau oleh kebetulan. Perlu pula diingat bahwa pilihan default dapat berubah antarversi JDK, seperti yang terjadi ketika G1 menggantikan Parallel sebagai kolektor bawaan di JDK 9. Mengganti kolektor karena ikut-ikutan hanya memindahkan masalah, bukan menyelesaikannya.

Ringkasan#

  • Kolektor adalah implementasi algoritma GC yang dapat ditukar lewat flag tanpa mengubah kode.
  • Serial memakai satu thread dan cocok untuk heap kecil, sedangkan Parallel memakai banyak thread demi throughput.
  • G1 membagi heap menjadi region dan mengejar target jeda yang bisa diatur, menjadi default sejak JDK 9.
  • ZGC bekerja hampir seluruhnya konkuren dengan jeda milidetik, dengan harga overhead throughput.
  • Nama GarbageCollectorMXBean memperlihatkan kolektor yang aktif beserta fasenya.

Referensi#