Bahasa seperti C menyerahkan urusan memori ke programmer: alokasikan, lalu lepaskan sendiri. Java memilih jalan sebaliknya — kita membuat objek sebebas-bebasnya dengan new, dan garbage collector (GC) yang memutuskan kapan objek benar-benar tidak dipakai lagi lalu memungut areanya untuk dipakai ulang. Pertanyaan kuncinya bukan “bagaimana memungut”, melainkan “bagaimana tahu objek sudah matangu” — dan jawaban JVM mungkin berbeda dari dugaan banyak orang. Pada chapter ini kita bedah cara GC menentukan hidup dan mati sebuah objek, membuktikannya dengan dua program observasi, lalu melihat algoritma dasar di balik proses pemungutan.

Menentukan Hidup dari Akar #

GC tidak menghitung berapa banyak referensi yang menunjuk sebuah objek — teknik reference counting itu rapuh karena dua objek dapat saling menunjuk membentuk siklus yang tak pernah habis hitungannya. Sebagai gantinya, JVM menjalankan reachability analysis: dimulai dari sekumpulan GC roots — titik yang pasti hidup — lalu menelusuri seluruh rantai referensi. Objek yang terjangkau dari akar dinyatakan hidup; sisanya, apa pun jumlah penunjuknya, adalah sampah. GC roots mencakup antara lain:

  • variabel lokal dan parameter pada frame method yang sedang berjalan di semua thread,
  • field static dari class yang sudah terinisialisasi,
  • objek yang terdaftar di JNI dan referensi khusus seperti Thread yang masih hidup.

Konsekuensinya sederhana namun kuat: objek tidak perlu “dihapus” secara eksplisit. Cukup pastikan tidak ada lagi jalan menuju objek tersebut — variabel lokal habis umur saat method selesai, field static habis umur saat class di-unload — dan GC akan menganggapnya sampah pada koleksi berikutnya.

Membuktikan dengan Program #

Bagaimana kita mengamati keputusan GC, yang memang tidak mencetak apa pun secara bawaan? Triknya adalah jendela observasi berupa WeakReference — pembungkus referensi yang tidak dihitung sebagai penunjuk hidup, sehingga get()-nya mengembalikan null tepat ketika objek yang dibungkusnya sudah dipungut. Program pertama menguji akar berupa variabel lokal:

import java.lang.ref.WeakReference;

public class PengamatRoot {
    static WeakReference<StringBuilder> pengamat;

    public static void main(String[] args) {
        buatData();
        System.gc();
        if (pengamat.get() == null) {
            System.out.println("setelah frame hilang + gc(): objek dipungut");
        } else {
            System.out.println("setelah frame hilang + gc(): objek masih ada");
        }
    }

    static void buatData() {
        StringBuilder lokal = new StringBuilder("data sementara");
        pengamat = new WeakReference<>(lokal);
        System.out.println("di dalam method : objek terlihat, isi = " + pengamat.get());
    }
}

Output yang Dihasilkan:

text
di dalam method : objek terlihat, isi = data sementara
setelah frame hilang + gc(): objek dipungut

Mari kita telaah alurnya:

  1. Baris 18-19 (buatData memanggil new StringBuilder): objek lahir di heap, ditunjuk variabel lokal lokal — variabel ini adalah GC root selama frame buatData masih hidup.
  2. Baris 20 (pengamat = new WeakReference<>(lokal)): jendela observasi dipasang. Karena WeakReference bukan penunjuk hidup, hidup-matinya objek kini sepenuhnya bergantung pada lokal.
  3. Baris 12 (buatData() selesai): frame method dibongkar — variabel lokal ikut mati, dan seluruh jalur ke objek terputus. Pemanggilan System.gc() lalu memicu koleksi, dan baris terakhir output menegaskan objek sudah dipungut.

Siklus Bukan Penghalang #

Bukti kedua sekaligus bantahan terhadap bayangan reference counting: dua objek yang saling menunjuk erat tetap dipungut begitu penunjuk dari luar hilang. Dengan penghitungan referensi, pasangan ini tidak akan pernah terkumpulkan — hitungan masing-masing selalu satu. Dengan reachability analysis, keduanya sampah karena tidak ada akar yang menjangkaunya:

import java.lang.ref.WeakReference;

public class SiklusTerungut {
    static class Node {
        Node tetangga;
    }

    public static void main(String[] args) {
        Node a = new Node();
        Node b = new Node();
        a.tetangga = b; // a menunjuk b
        b.tetangga = a; // b menunjuk a: terbentuk siklus

        WeakReference<Node> pengamatA = new WeakReference<>(a);
        WeakReference<Node> pengamatB = new WeakReference<>(b);

        a = null; // kedua referensi luar dilepas
        b = null; // objek saling menunjuk satu sama lain
        System.gc();

        System.out.println("pengamatA.get() = " + pengamatA.get());
        System.out.println("pengamatB.get() = " + pengamatB.get());
    }
}

Output yang Dihasilkan:

text
pengamatA.get() = null
pengamatB.get() = null

Dua baris null pada output adalah kesimpulan utuhnya: saling menunjuk tidak menyelamatkan objek dari pemungutan, karena penelusuran dari GC roots tidak pernah sampai ke keduanya setelah a dan b di-nol-kan. Inilah alasan struktur data berjejaring — peta, graph, cache bersilang — tidak perlu pembongkaran manual di Java.

Mark, Sweep, dan Compact #

Setelah objek hidup diketahui, bagaimana areanya direbut kembali? Algoritma dasarnya bekerja tiga fase. Mark: GC menandai seluruh objek yang terjangkau dari roots, biasanya dengan mengeser bit penanda di header objek. Sweep: area objek tak bertanda dinyatakan bebas dan dikembalikan ke daftar ruang kosong. Compact: objek hidup digeser merapat ke satu sisi sehingga ruang bebas menyatu — mencegah fragmentasi dan menyederhanakan alokasi berikutnya menjadi sekadar menggeser penunjuk.

Fase mark dan compact menghentikan seluruh thread aplikasi Java sejenak — momen stop-the-world — karena penandaan tidak boleh kejar-kejaran dengan objek yang berpindah. Karena itu ukuran dan frekuensi jeda menjadi ukuran mutu sebuah collector, dan inilah pembeda antara Serial, Parallel, G1, hingga ZGC yang akan kita bandingkan pada Chapter Jenis Garbage Collector. Sebagian collector modern memperkecil jeda dengan menandai secara concurrent bersama thread aplikasi yang terus berjalan.

Mati Proses Bukan Urusan GC #

Sebagai catatan penutup yang kerap disalahpahami: garbage collector adalah urusan selama program hidup — memakai ulang memori agar aplikasi bisa berjalan lama tanpa membengkak. Ketika proses Java berakhir, sistem operasi melepas seluruh memori proses tersebut sekaligus, selesai atau tidak GC sempat bekerja. Maka mematikan aplikasi lewat kill -9 tidak meninggalkan “sampah” di mesin — yang hilang hanyalah kesempatan shutdown gracefully: menyiram data ke disk, menutup koneksi, dan menjalankan hook Runtime.addShutdownHook. Sebaliknya, kebocoran memori di aplikasi berumur panjang tetap merupakan bencana nyata meski proses akan dimatikan akhirnya — pokok bahasan Chapter Memory Leaks.

Peta konsep GC kini lengkap: akar penentu hidup, algoritma pemungutan, dan jeda stop-the-world. Chapter berikutnya memperkenalkan empat collector HotSpot yang mengimplementasikan konsep-konsep ini dengan karakter jeda dan throughput yang berbeda-beda — dan kriteria memilihnya.

Referensi #

Berkas Sumber

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

PengamatRoot.java

Unduh berkas
java
import java.lang.ref.WeakReference;

public class PengamatRoot {
    static WeakReference<StringBuilder> pengamat;

    public static void main(String[] args) {
        buatData();
        System.gc();
        if (pengamat.get() == null) {
            System.out.println("setelah frame hilang + gc(): objek dipungut");
        } else {
            System.out.println("setelah frame hilang + gc(): objek masih ada");
        }
    }

    static void buatData() {
        StringBuilder lokal = new StringBuilder("data sementara");
        pengamat = new WeakReference<>(lokal);
        System.out.println("di dalam method : objek terlihat, isi = " + pengamat.get());
    }
}

SiklusTerungut.java

Unduh berkas
java
import java.lang.ref.WeakReference;

public class SiklusTerungut {
    static class Node {
        Node tetangga;
    }

    public static void main(String[] args) {
        Node a = new Node();
        Node b = new Node();
        a.tetangga = b; // a menunjuk b
        b.tetangga = a; // b menunjuk a: terbentuk siklus

        WeakReference<Node> pengamatA = new WeakReference<>(a);
        WeakReference<Node> pengamatB = new WeakReference<>(b);

        a = null; // kedua referensi luar dilepas
        b = null; // objek saling menunjuk satu sama lain
        System.gc();

        System.out.println("pengamatA.get() = " + pengamatA.get());
        System.out.println("pengamatB.get() = " + pengamatB.get());
    }
}