Di bahasa seperti C, programmer mengelola memori sendiri: mengalokasikan dengan malloc dan mengembalikannya dengan free. Java memotong beban tersebut — kita menciptakan objek dengan new, dan garbage collector (GC) di dalam JVM yang memungut objek ketika objek tersebut sudah tidak dipakai lagi. Meski otomatis, siklus hidup objek tetap perlu dipahami: kebocoran referensi yang tidak disengaja membuat objek hidup lebih lama dari yang dibutuhkan, dan kebiasaan menaruh null di semua tempat justru menandakan desain yang kurang bersih. Pada Chapter ini kita ikuti perjalanan objek dari penciptaan, pemakaian, hingga menjadi eligible for garbage collection — tanpa menebak kapan GC benar-benar berjalan.

Perjalanan Sebuah Objek #

Siklus hidup objek Java dapat diringkas menjadi empat tahap berurutan:

Tahap penciptaan dimulai ketika JVM menemukan new: memori untuk objek dialokasikan di heap, field diberi nilai awal (0, false, atau null sesuai tipenya), lalu initializer dan constructor berjalan mengisi state sesuai keinginan kita. Tahap pemakaian berlangsung selama ada referensi yang menjangkau objek — lewat variabel, field objek lain, atau elemen array. Tahap ketiga dimulai ketika referensi terakhir yang menjangkau objek hilang; saat itu objek menjadi unreachable dan berstatus eligible for GC — kandidat yang boleh dimungut JVM. GC lalu mengambil keputusan atas kandidat tersebut dengan penjadwalan miliknya sendiri.

Mengamati Tahapan dalam Kode #

Contoh berikut memakai domain rekening dan mencetak penanda pada setiap tahap. Dua variabel sengaja diarahkan ke objek yang sama untuk menunjukkan perilaku alias — satu objek dengan dua pintu masuk:

class Rekening {
    private final String pemilik;
    private double saldo;

    public Rekening(String pemilik, double saldoAwal) {
        this.pemilik = pemilik;
        this.saldo = saldoAwal;
        System.out.println("[cipta] Rekening " + pemilik + " dibuat");
    }

    public void setor(double jumlah) {
        saldo += jumlah;
        System.out.println("[pakai] " + pemilik + " setor Rp" + (int) jumlah);
    }

    public double saldo() {
        return saldo;
    }
}

public class TahapanSiklus {
    public static void main(String[] args) {
        // Tahap 1: penciptaan — alokasi memori, inisialisasi field, constructor.
        Rekening rekening = new Rekening("Budi", 500000);

        // Tahap 2: pemakaian — objek diakses lewat referensinya.
        rekening.setor(250000);
        System.out.println("[pakai] saldo sekarang: Rp" + (int) rekening.saldo());

        // Alias: dua variabel menunjuk objek yang sama.
        Rekening alias = rekening;
        alias.setor(50000);
        System.out.println("[pakai] saldo lewat rekening: Rp" + (int) rekening.saldo());

        // Tahap 3: referensi dilepas satu per satu.
        alias = null;
        System.out.println("[lepas] alias = null; objek masih dipegang rekening");
        rekening = null;
        System.out.println("[lepas] rekening = null; objek eligible untuk GC");
    }
}

Output yang Dihasilkan:

text
[cipta] Rekening Budi dibuat
[pakai] Budi setor Rp250000
[pakai] saldo sekarang: Rp750000
[pakai] Budi setor Rp50000
[pakai] saldo lewat rekening: Rp800000
[lepas] alias = null; objek masih dipegang rekening
[lepas] rekening = null; objek eligible untuk GC

Mari kita bedah baris-baris pentingnya:

  1. new Rekening("Budi", 500000): satu ekspresi ini memuat tiga aksi berurutan — JVM mengalokasikan memori objek, mengisi nilai awal field, lalu menjalankan constructor yang mencetak baris [cipta].
  2. Rekening alias = rekening;: penugasan referensi tidak menyalin objek. Variabel alias dan rekening kini menunjuk objek yang sama, sehingga setoran lewat alias terlihat juga dari rekening (saldo Rp800.000).
  3. alias = null;: melepas satu alias tidak memengaruhi objek selama referensi lain masih memegangnya. Objek baru kehilangan jangkauan setelah variabel rekening juga di-set null.
  4. Dua baris [lepas]: status eligible untuk GC tercapai hanya pada baris kedua. GC boleh memungut objek mulai saat itu — tetapi tidak ada jaminan kapan pemungutan terjadi, dan program kita sama sekali tidak bergantung pada momen tersebut.

Lingkup Method sebagai Batas Hidup #

Mengosongkan variabel manual dengan null sebenarnya jarang diperlukan. Cara paling alami memutus jangkauan adalah membiarkan variabel keluar dari lingkupnya — misalnya saat method selesai:

class Laporan {
    private final String judul;

    public Laporan(String judul) {
        this.judul = judul;
        System.out.println("[cipta] Laporan " + judul + " disiapkan");
    }

    public void cetak() {
        System.out.println("[pakai] Laporan " + judul + " dicetak");
    }
}

public class LingkupSiklus {
    static void buatLaporanSementara() {
        Laporan laporan = new Laporan("Penjualan");
        laporan.cetak();
        // Variabel laporan berakhir di sini. Setelah method kembali,
        // tidak ada lagi referensi yang menunjuk objek tersebut.
    }

    public static void main(String[] args) {
        buatLaporanSementara();
        System.out.println("Method selesai; objek laporan tidak lagi terjangkau");

        // Saran kepada JVM untuk menjalankan pemungutan sekarang.
        // JVM bebas mengabaikannya; bukan perintah yang mengikat.
        System.gc();
        System.out.println("System.gc() hanyalah saran; program tetap berjalan");
    }
}

Output yang Dihasilkan:

text
[cipta] Laporan Penjualan disiapkan
[pakai] Laporan Penjualan dicetak
Method selesai; objek laporan tidak lagi terjangkau
System.gc() hanyalah saran; program tetap berjalan

Objek Laporan diciptakan sebagai variabel lokal di dalam buatLaporanSementara() dan tidak pernah dikembalikan ke pemanggil. Begitu method tersebut selesai, satu-satunya referensi berakhir bersama stack frame-nya — tanpa satu baris null pun. Pola ini adalah cara utama membatasi umur objek: ciptakan objek sedekat mungkin dengan pemakaiannya dan biarkan lingkup bekerja.

Peran Garbage Collector #

GC menentukan sendiri kapan pemungutan dilakukan berdasarkan tekanan memori dan strategi JVM — kita tidak bisa memaksakan jadwalnya. Panggilan System.gc() hanyalah saran: JVM boleh menjalankan GC segera, menunda, atau mengabaikannya. Karena itu kode yang benar tidak pernah bergantung pada urutan atau waktu pemungutan; status eligible sudah cukup sebagai jaminan logis bahwa memori bisa kembali dipakai.

Objek yang sudah terbiasa hidup dalam lingkup yang rapi kini membawa kita ke pertanyaan berikutnya: bagaimana membandingkan dua objek? Operator == membandingkan referensi, sedangkan kesamaan nilai logis menuntut aturan lain. Chapter Object Equality membedah keduanya.

Referensi #

Berkas Sumber

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

LingkupSiklus.java

Unduh berkas
java
class Laporan {
    private final String judul;

    public Laporan(String judul) {
        this.judul = judul;
        System.out.println("[cipta] Laporan " + judul + " disiapkan");
    }

    public void cetak() {
        System.out.println("[pakai] Laporan " + judul + " dicetak");
    }
}

public class LingkupSiklus {
    static void buatLaporanSementara() {
        Laporan laporan = new Laporan("Penjualan");
        laporan.cetak();
        // Variabel laporan berakhir di sini. Setelah method kembali,
        // tidak ada lagi referensi yang menunjuk objek tersebut.
    }

    public static void main(String[] args) {
        buatLaporanSementara();
        System.out.println("Method selesai; objek laporan tidak lagi terjangkau");

        // Saran kepada JVM untuk menjalankan pemungutan sekarang.
        // JVM bebas mengabaikannya; bukan perintah yang mengikat.
        System.gc();
        System.out.println("System.gc() hanyalah saran; program tetap berjalan");
    }
}

TahapanSiklus.java

Unduh berkas
java
class Rekening {
    private final String pemilik;
    private double saldo;

    public Rekening(String pemilik, double saldoAwal) {
        this.pemilik = pemilik;
        this.saldo = saldoAwal;
        System.out.println("[cipta] Rekening " + pemilik + " dibuat");
    }

    public void setor(double jumlah) {
        saldo += jumlah;
        System.out.println("[pakai] " + pemilik + " setor Rp" + (int) jumlah);
    }

    public double saldo() {
        return saldo;
    }
}

public class TahapanSiklus {
    public static void main(String[] args) {
        // Tahap 1: penciptaan — alokasi memori, inisialisasi field, constructor.
        Rekening rekening = new Rekening("Budi", 500000);

        // Tahap 2: pemakaian — objek diakses lewat referensinya.
        rekening.setor(250000);
        System.out.println("[pakai] saldo sekarang: Rp" + (int) rekening.saldo());

        // Alias: dua variabel menunjuk objek yang sama.
        Rekening alias = rekening;
        alias.setor(50000);
        System.out.println("[pakai] saldo lewat rekening: Rp" + (int) rekening.saldo());

        // Tahap 3: referensi dilepas satu per satu.
        alias = null;
        System.out.println("[lepas] alias = null; objek masih dipegang rekening");
        rekening = null;
        System.out.println("[lepas] rekening = null; objek eligible untuk GC");
    }
}