Garbage Collection

Memahami reachability, GC generasional young dan old, serta jeda stop-the-world.

Daftar isi

Objek yang tidak dirujuk lagi tetap menempati heap sampai garbage collection (GC) mengambilnya. Topik ini menjelaskan cara JVM menentukan objek yang masih terpakai lewat konsep reachability, struktur heap generasional yang menopang kecepatan GC, serta jeda stop-the-world yang menyertainya. Pengamatan jangkauan dilakukan lewat program kecil yang dapat dijalankan langsung, sehingga konsepnya tidak berhenti sebagai teori.

Menentukan objek yang masih terpakai#

GC tidak menebak objek mana yang tidak terpakai, melainkan menelusuri rantai referensi dari titik awal yang pasti masih hidup. Titik awal itu disebut GC root, contohnya variabel lokal di setiap frame stack, field static yang kelasnya masih dimuat, dan referensi yang sedang dipegang thread yang berjalan. Daftar rootnya tidak berhenti di tiga contoh tersebut karena ada sumber lain seperti referensi yang sedang dipakai kode native, tetapi tiga sumber utama itu sudah mencakup kasus sehari-hari. Objek yang dapat dicapai lewat rantai referensi dari root disebut terjangkau (reachable), sedangkan sisanya dianggap sampah. Pendekatan penelusuran ini menghindari kelemahan penghitungan referensi sederhana, misalnya dua objek yang saling menunjuk tetapi tidak terjangkau dari root pun tetap dikumpulkan.

Kata kuncinya adalah rantai referensi. Selama satu jalur dari root sampai objek masih ada, GC menolak membuang objek itu, seberapa pun lamanya objek tersebut tidak dipakai. Inilah fondasi pembahasan kebocoran memori pada akhir modul ini, yaitu objek yang secara logis sudah tidak berguna tetapi masih terjangkau dari root.

Mengamati jangkauan lewat WeakReference#

Jangkauan tidak dapat ditanya langsung kepada JVM dari dalam program, tetapi kelas WeakReference bisa dipakai sebagai alat pengamatan. Objek yang dibungkus WeakReference tidak dihitung terjangkau lewat referensinya, dan metode get() mengembalikan null begitu objeknya tidak lagi terjangkau dari root. Detail keempat tipe referensi dibahas pada topik tersendiri, sedangkan di sini WeakReference hanya dipakai sebagai sensor.

java UjiJangkauan.java
import java.lang.ref.WeakReference;

public class UjiJangkauan {

    public static void main(String[] args) {
        Object data = new Object();
        WeakReference<Object> pengamat = new WeakReference<>(data);

        System.gc();
        System.out.println("masih dirujuk variabel lokal : " + (pengamat.get() != null));

        data = null;
        System.gc();
        System.out.println("tidak dirujuk siapa pun lagi : " + (pengamat.get() == null));
    }
}
text Keluaran
masih dirujuk variabel lokal : true
tidak dirujuk siapa pun lagi : true

Keluarannya menunjukkan dua fase. Pada fase pertama, variabel lokal data masih menunjuk objek, sehingga objek terjangkau dari root dan tetap hidup meski System.gc() dipanggil. Pada fase kedua, data diberi nilai null sehingga satu-satunya jalur menuju objek hilang, dan GC mengosongkan referensi lemahnya. Perhatikan bahwa program tidak pernah memanggil metode apa pun untuk membuang objek, karena penghapusan sepenuhnya menjadi urusan GC.

Sensor semacam ini juga menjadi dasar alat diagnostik. Alat pemantau heap menandai objek yang hanya terjangkau lewat referensi lemah sebagai kandidat pembuangan, dan demo di atas mengikuti logika yang sama dalam skala kecil. Selisih antara dua fase hanya penugasan null pada satu variabel, yang menegaskan bahwa keputusan pembuangan sepenuhnya bergantung pada keberadaan jalur referensi, bukan pada lamanya objek ditinggalkan.

Heap generasional: Eden, Survivor, dan Old#

Kebanyakan objek dalam aplikasi nyata berumur sangat pendek, misalnya objek sementara yang lahir dan mati dalam satu pemanggilan metode. Pengamatan ini dikenal sebagai weak generational hypothesis, dan JVM memanfaatkannya dengan membagi heap menjadi generasi. Objek baru dialokasikan di ruang bernama Eden pada generasi muda (young generation). Ketika Eden penuh, GC berjalan dalam bentuk minor GC yang cepat, yaitu menyalin objek yang masih hidup ke salah satu ruang survivor sambil membuang sisanya.

Setiap kali objek lolos dari satu siklus minor GC, umurnya dihitung, dan objek yang bertahan melewati ambang tertentu dipromosikan ke generasi tua (old generation). Generasi tua dikumpulkan lebih jarang, biasanya ketika ruangnya mulai menipis, dan prosesnya lebih mahal karena objek yang hidup jauh lebih banyak. Pembagian ruangnya digambarkan berikut.

HeapGenerasi mudaEdenalokasi objek baruSurvivorSurvivorGenerasi tuaobjek berumur panjangminor GCpromosi
Heap generasional: Eden, dua ruang survivor, dan generasi tua

Pemilihan desain ini membuat pekerjaan GC terbagi sesuai umur objek. Minor GC menyapu area kecil yang penuh objek sesaat sehingga selesai dalam milidetik, sedangkan pengumpulan generasi tua lebih jarang tetapi lebih berat. Aplikasi dengan tingkat alokasi tinggi merasakan perbedaan ini langsung pada frekuensi jeda yang muncul di profilnya.

Jeda stop-the-world#

Saat GC memindahkan objek, GC perlu melihat heap dalam keadaan beku agar tidak ada referensi yang berubah di tengah penelusuran. Pada fase itu semua thread aplikasi dihentikan sesaat, keadaan yang dikenal sebagai stop-the-world (STW). Minor GC biasanya berjalan cepat sehingga jeda yang timbul nyaris tidak terasa, sedangkan pengumpulan penuh (full GC) bisa menyita waktu jauh lebih lama karena menyapu seluruh heap. Aplikasi yang sensitif terhadap latensi merasakan jeda ini sebagai lonjakan waktu respons yang muncul berkala.

Event GC dapat diamati dari luar program dengan flag -Xlog:gc pada perintah java, dan blok berikut menunjukkan bentuk perintahnya.

bash
# tidak-dijalankan
# menampilkan event GC beserta durasi jeda ke terminal
java -Xlog:gc UjiJangkauan.java

Log yang muncul memuat baris dengan pola Pause Young untuk minor GC dan Pause Full untuk pengumpulan penuh, lengkap dengan durasi jeda dalam milidetik. Angka pastinya berubah setiap eksekusi karena bergantung pada ukuran heap dan tingkat alokasi, sehingga lognya tidak ditempel di sini. Yang penting dipahami adalah setiap baris tersebut mewakili jeda yang dialami aplikasi secara nyata.

Kesalahan umum#

  • Memanggil System.gc() di tengah logika berharap memori langsung bersih. Pemanggilan itu memicu jeda penuh yang mahal tanpa jaminan objek yang diharapkan ikut terkumpul, karena persyaratannya tetap reachability. Biarkan GC bekerja sesuai jadwalnya.
  • Menugaskan null ke semua variabel sebelum GC berjalan. Penugasan null hanya perlu untuk referensi yang umurnya panjang, misalnya slot array besar yang sudah selesai dipakai. Variabel lokal biasanya mati bersama framanya.
  • Menganggap GC menjamin bebas kebocoran. GC hanya membuang objek yang tidak terjangkau, sehingga objek yang masih dirujuk tanpa pernah dipakai lagi tetap tinggal. Pola kebocorannya dibahas pada akhir modul ini.
  • Menyamakan minor GC dengan full GC. Keduanya menyapu area berbeda dengan biaya berbeda, sehingga menggabungkan keduanya dalam satu angka membuat diagnosis latensi menjadi keliru.

Ringkasan#

  • GC menentukan objek sampah lewat reachability dari GC root, bukan lewat penghitungan pengguna.
  • Selama satu jalur referensi dari root masih ada, objek tetap hidup, dan objek yang tidak terjangkau boleh dikumpulkan.
  • Heap generasional memisahkan Eden, ruang survivor, dan generasi tua agar minor GC tetap cepat.
  • Fase GC tertentu menghentikan semua thread aplikasi (stop-the-world), dan flag -Xlog:gc memperlihatkan jeda tersebut.

Referensi#