Data yang tinggal di heap tidak dibongkar sendiri ketika fungsinya selesai — sesuatu harus datang membersihkannya. Di Go, tugas tersebut dipegang garbage collector (GC) yang berjalan otomatis di dalam runtime, sehingga kita tidak pernah menulis free atau delete seperti di C dan C++. Otomatisasi ini bukan tanpa konsekuensi: siklus GC memakan CPU, dan pemahaman tentang cara kerjanya menentukan kemampuan kita menyetel program yang efisien. Chapter ini merangkum cara GC bekerja, memperkenalkan tombol pengaturnya, lalu membuktikan geraknya dengan pengukuran nyata.

Cara Kerja Singkat: Tandai lalu Sapu #

GC Go memakai keluarga algoritma mark and sweep dengan penandaan tri-color. Fase penandaan dimulai dari sekumpulan akar — variabel global serta seluruh stack goroutine yang sedang berjalan — lalu menelusuri setiap pointer untuk menandai objek yang masih dapat dijangkau. Objek heap yang tidak mendapat tanda dianggap sampah, dan fase penyapuan kemudian membebaskan memorinya untuk dipakai ulang. Seluruh proses ini berjalan konkuren dengan program kita: goroutine aplikasi tetap mengerjakan tugasnya, dengan jeda-jeda sangat pendek di titik koordinasi tertentu. Kesimpulan praktisnya satu kalimat: selama sebuah objek masih dirujuk dari mana pun yang hidup, GC tidak akan membebaskannya.

Mengatur Semangat Kerja GC #

Runtime menyediakan beberapa tombol pengatur. Variabel lingkungan GOGC (nilai bawaan 100) menentukan seberapa jauh heap boleh tumbuh relatif terhadap data hidup sebelum satu siklus GC dipicu — nilai 100 berarti GC berjalan ketika heap mencapai sekitar dua kali ukuran data hidup. Nilai yang lebih besar menghasilkan GC yang lebih jarang tapi heap lebih gemuk; nilai yang lebih kecil melakukan sebaliknya. Dari dalam program, fungsi debug.SetGCPercent mengubah angka yang sama saat runtime, dan fungsi runtime.GC() memaksa satu putaran pengumpulan penuh secara segera — berguna untuk pengujian dan pengukuran. Sejak Go 1.19 tersedia pula GOMEMLIMIT beserta pasangan programnya debug.SetMemoryLimit: batas lunak total memori yang membuat GC makin agresif ketika pemakaian memori mendekati batas. Pendalaman setelan produksi tersedia pada chapter Garbage Collection Tuning .

Contoh Utuh: Mematikan lalu Membangunkan GC #

Program berikut mematikan GC, mengalokasikan 64 MiB data sampah, mengukur pertumbuhan heap, lalu membangunkan GC dan mengukur penyusutannya:

go
package main

import (
	"fmt"
	"runtime"
	"runtime/debug"
)

// buatSampah mengalokasikan 64 MiB data yang segera tidak terpakai lagi.
func buatSampah() {
	for range 64 {
		_ = make([]byte, 1024*1024)
	}
}

// heapSekarang membaca jumlah byte heap yang sedang dipakai.
func heapSekarang() uint64 {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	return m.HeapAlloc
}

func main() {
	debug.SetGCPercent(-1) // matikan GC sementara
	runtime.GC()           // bersihkan sampah sisa proses awal program

	sebelum := heapSekarang()
	buatSampah()
	sesudah := heapSekarang()

	debug.SetGCPercent(100) // aktifkan kembali dengan target bawaan
	runtime.GC()            // paksa pengumpulan sampah sekarang

	setelahGC := heapSekarang()

	fmt.Println("heap bertumbuh tanpa GC  :", sesudah > sebelum)
	fmt.Println("heap menyusut setelah GC :", setelahGC < sesudah)
}

Jalankan program tersebut dari direktori chapter dengan perintah berikut:

bash
go run garbage-collection-demo.go

Output yang Dihasilkan:

text
heap bertumbuh tanpa GC  : true
heap menyusut setelah GC : true

Mari kita telaah baris demi baris program di atas:

  1. Baris 10-14 (buatSampah): fungsi ini mengalokasikan 64 buffer 1 MiB yang langsung dibuang; tanpa GC aktif, seluruh 64 MiB menumpuk di heap.
  2. Baris 17-21 (heapSekarang): runtime.ReadMemStats mengisi struct MemStats dengan statistik runtime; field HeapAlloc melaporkan byte heap yang sedang dipakai.
  3. Baris 24-25 (SetGCPercent(-1)): nilai negatif mematikan GC otomatis; runtime.GC() tetap sah dan dipakai untuk membersihkan sisa alokasi proses awal program.
  4. Baris 27-29 (buatSampah()): pengukuran pertama membuktikan heap bertumbuh — sampah tidak pernah diambil ketika GC mati.
  5. Baris 31-32 (SetGCPercent(100)): GC diaktifkan kembali, lalu runtime.GC() memaksa satu pengumpulan segera.
  6. Baris 34-37 (setelahGC := heapSekarang()): pengukuran kedua membuktikan heap menyusut karena seluruh sampah telah dibebaskan.

GC Tidak Bisa Membebaskan yang Masih Dirujuk #

Dua batasan GC perlu dipahami agar ekspektasi kita tepat. Pertama, GC hanya membebaskan memori; GC tidak menghentikan goroutine yang masih berjalan atau menutup channel yang terlupakan. Kedua, GC menuruti definisi sampahnya sendiri: memori yang masih dirujuk — bahkan oleh satu closure kecil atau satu entri map — bukan sampah dan tidak akan dibebaskan. Karena itu kebocoran memori di Go hampir selalu berwujud referensi tersembunyi yang membuat data hidup lebih lama dari kebutuhan, bukan kegagalan GC. Pola-pola referensi tersembunyi tersebut beserta cara mendeteksinya menjadi materi penutup section ini pada chapter berikutnya.

Referensi #

GC bekerja otomatis, tetapi kebersihan memori tetap tanggung jawab kita sebagai penulis kode. Chapter terakhir section ini melengkapi pertahanan itu: mengenali pola kebocoran klasik dan membuktikannya dengan angka.

Berkas Sumber

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

garbage-collection-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"runtime"
	"runtime/debug"
)

// buatSampah mengalokasikan 64 MiB data yang segera tidak terpakai lagi.
func buatSampah() {
	for range 64 {
		_ = make([]byte, 1024*1024)
	}
}

// heapSekarang membaca jumlah byte heap yang sedang dipakai.
func heapSekarang() uint64 {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	return m.HeapAlloc
}

func main() {
	debug.SetGCPercent(-1) // matikan GC sementara
	runtime.GC()           // bersihkan sampah sisa proses awal program

	sebelum := heapSekarang()
	buatSampah()
	sesudah := heapSekarang()

	debug.SetGCPercent(100) // aktifkan kembali dengan target bawaan
	runtime.GC()            // paksa pengumpulan sampah sekarang

	setelahGC := heapSekarang()

	fmt.Println("heap bertumbuh tanpa GC  :", sesudah > sebelum)
	fmt.Println("heap menyusut setelah GC :", setelahGC < sesudah)
}