Bahasa dengan manajemen memori otomatis seperti Kotlin/JVM membebaskan kita dari keharusan membebaskan memori secara manual. Tugas tersebut dipegang oleh garbage collector (GC) yang berjalan di dalam JVM. Pemahaman cara kerja GC menentukan apakah kita mampu memilih algoritma yang sesuai dengan profil aplikasi, mengenali stop-the-world pause yang memengaruhi latensi, dan mengoptimalkan pola alokasi objek. Pada chapter ini kita akan membahas algoritma mark-and-sweep, struktur generational heap, kolektor modern (G1, ZGC, Shenandoah), serta cara kerja stop-the-world.

Algoritma Mark-and-Sweep #

Algoritma dasar GC yang dipakai oleh JVM adalah mark-and-sweep. Proses ini berjalan dalam dua fase:

  1. Mark: dimulai dari GC roots (variabel lokal di stack, static field, referensi JNI), GC menelusuri grafik referensi objek dan menandai setiap objek yang masih dapat dijangkau (reachable).
  2. Sweep: GC memindai seluruh heap dan membebaskan memori yang ditempati objek yang tidak ditandai.

Kelemahan utama mark-and-sweep murni adalah fragmentasi memori dan waktu jeda yang proporsional dengan ukuran heap. Karena alasan ini, JVM modern memakai optimasi generational dan concurrent untuk mengurangi jeda.

Generational Heap #

Pengamatan empiris pada aplikasi nyata menunjukkan bahwa mayoritas objek berumur pendek — dibuat, dipakai sekali, lalu tidak lagi dirujuk. Berdasarkan observasi ini, JVM membagi heap menjadi beberapa generasi:

GenerasiIsiFrekuensi GCAlgoritma khas
Young (Eden + Survivor 0 + Survivor 1)Objek baruSering, cepatCopying
Old (Tenured)Objek yang bertahan beberapa GC minorJarang, lebih lamaMark-sweep-compact
MetaspaceMetadata class, method (sebelumnya PermGen)Sesuai pemuatan classNative

Saat Eden penuh, minor GC memindai young generation, menyalin objek yang masih hidup ke salah satu survivor space, dan membebaskan sisanya. Objek yang sudah bertahan beberapa kali minor GC dipromosikan ke old generation. Saat old generation penuh, terjadi major GC (juga disebut full GC) yang membersihkan seluruh heap.

Kolektor Modern #

JVM modern menyediakan beberapa pilihan GC, masing-masing dengan tradeoff antara throughput dan latency:

  • G1 (Garbage First): default sejak JDK 9. Mempartisi heap menjadi region-region kecil; memilih region dengan paling banyak garbage untuk dikoleksi lebih dulu. Pause time target dapat diatur via -XX:MaxGCPauseMillis.
  • ZGC: kolektor low-latency sejak JDK 15 (produksi). Pause time di bawah 1 ms bahkan untuk heap multi-TB. Memakai colored pointers dan load barriers.
  • Shenandoah: kolektor concurrent dari proyek OpenJDK. Pause time konsisten di bawah 10 ms dengan algoritma concurrent evacuation via brooks pointers.
  • Parallel GC: throughput-oriented, cocok untuk pekerjaan batch. Default sebelum JDK 9.
  • Serial GC: untuk heap kecil dan aplikasi sederhana (-XX:+UseSerialGC).

Demonstrasi Pola Reference #

Kode berikut memperlihatkan tiga pola reference yang relevan dengan GC: WeakReference untuk cache ringan, SoftReference untuk cache yang bertahan sampai memori rendah, dan pola AutoCloseable + .use untuk cleanup deterministik.

kotlin
import java.lang.ref.WeakReference
import java.lang.ref.SoftReference

fun main() {
    println("=== Demonstrasi Pola Reference dan Cleanup ===")

    // 1. Pola: WeakReference sebagai cache ringan
    var kuat: String? = "data-cache-c1a2b3"
    val lemah = WeakReference(kuat!!)
    println("Pola 1: WeakReference.get() saat strong masih hidup = ${lemah.get()}")
    kuat = null
    println("Pola 1: Setelah strong=null, WeakReference mungkin masih menyimpan atau sudah di-GC")
    println("         (perilaku tidak deterministik, jangan andalkan untuk logika bisnis)")

    // 2. Pola: SoftReference untuk cache yang bertahan sampai memori rendah
    val lembut = SoftReference("data-pendukung-9f8e7d")
    println()
    println("Pola 2: SoftReference.get() = ${lembut.get()}")
    println("         SoftReference lebih kuat dari WeakReference, bertahan sampai OOM dekat")

    // 3. Pola: cleanup deterministik dengan try-finally / .use
    println()
    println("Pola 3: Resource cleanup dengan .use (deterministik)")
    val resource = object : AutoCloseable {
        var terbuka = true
        override fun close() {
            terbuka = false
            println("  -> resource.close() dipanggil, status sekarang = $terbuka")
        }
    }
    resource.use {
        println("  -> di dalam blok use, resource terbuka = ${resource.terbuka}")
    }
    println("  -> setelah blok use, resource terbuka = ${resource.terbuka}")

    // 4. Pola: finalizer tidak deterministik (DEPRECATED sejak Java 18)
    println()
    println("Pola 4: Hindari finalize(); pakai AutoCloseable + try-with-resources (.use)")
    println("         Cleaner (Java 9+) adalah pengganti finalizer untuk cleanup native resource")
}

Output yang Dihasilkan:

text
=== Demonstrasi Pola Reference dan Cleanup ===
Pola 1: WeakReference.get() saat strong masih hidup = data-cache-c1a2b3
Pola 1: Setelah strong=null, WeakReference mungkin masih menyimpan atau sudah di-GC
         (perilaku tidak deterministik, jangan andalkan untuk logika bisnis)

Pola 2: SoftReference.get() = data-pendukung-9f8e7d
         SoftReference lebih kuat dari WeakReference, bertahan sampai OOM dekat

Pola 3: Resource cleanup dengan .use (deterministik)
  -> di dalam blok use, resource terbuka = true
  -> resource.close() dipanggil, status sekarang = false
  -> setelah blok use, resource terbuka = false

Pola 4: Hindari finalize(); pakai AutoCloseable + try-with-resources (.use)
         Cleaner (Java 9+) adalah pengganti finalizer untuk cleanup native resource

Mari kita telaah kode di atas:

  1. Pola 1 (WeakReference): referensi lemah tidak mencegah objek di-GC. Cocok untuk cache yang boleh hilang kapan saja. Setelah kuat = null, WeakReference mungkin masih menyimpan nilai atau sudah di-GC tergantung kapan GC berjalan — karenanya WeakReference tidak boleh dipakai untuk logika bisnis yang sensitif terhadap keberadaan objek.
  2. Pola 2 (SoftReference): lebih kuat dari WeakReference. JVM mempertahankan objek SoftReference sampai memori hampir habis. Cocok untuk memory-sensitive cache seperti cache gambar atau hasil dekompresi.
  3. Pola 3 (AutoCloseable + .use): cleanup deterministik. Method close() dipanggil tepat saat blok selesai, tidak menunggu GC. Pola ini dianjurkan untuk resource I/O, koneksi database, dan lock.
  4. Pola 4 (Catatan finalize()): finalizer telah di-deprecate sejak Java 18 karena tidak deterministik dan mahal. Penggantinya adalah Cleaner (Java 9+).

Stop-the-World Pauses #

Sebagian besar kolektor memerlukan momen singkat ketika semua thread aplikasi dihentikan untuk fase tertentu GC. Momen ini disebut stop-the-world pause. Untuk G1, pause biasanya 10-200 ms tergantung ukuran region. Untuk ZGC dan Shenandoah, pause di bawah 1 ms karena sebagian besar pekerjaan dilakukan concurrent dengan thread aplikasi.

bash
# Mengaktifkan log GC untuk mengamati pause time
java -Xlog:gc*=info,gc+pause=debug -jar aplikasi.jar
# Contoh output:
# [info] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 25M->3M(64M) 3.452ms

Referensi Lanjutan #

  • JVM Guide — panduan resmi HotSpot JVM Java SE 21 yang membahas garbage collector

Penutup #

GC modern telah membuat manajemen memori otomatis menjadi praktis tanpa mengurangi latensi secara signifikan. Pilihan kolektor dan tuning flag (-XX:MaxGCPauseMillis, -Xmx) bergantung pada karakteristik beban kerja. Pada chapter berikutnya kita akan membahas situasi kebalikannya: ketika objek tetap dirujuk walau seharusnya sudah tidak dipakai — yang dikenal sebagai memory leak.