Garbage collection Go: stack, heap, dan pointer
Go menawarkan kesepakatan yang jarang diberikan bahasa lain: Anda tidak pernah membebaskan memori sendiri.
Anda mengalokasi sebebasnya, dan satu kru pembersih di belakang layar — si garbage collectorⓘ — mengambil
kembali apa pun yang berhenti Anda pakai. Harga dari keamanan itu adalah sedikit waktu CPU dan ruang
cadangan. Artikel ini bercerita tentang di mana data Anda tinggal (sebuah buku catatan dan sebuah
gudang), kapan si pembersih datang (tombol GOGC), dan satu keputusan diam-diam yang memindahkan kerja
dari tempat murah ke tempat mahal: mengambil pointerⓘ *.
Kapan si pembersih datang — heuristik GOGC
Collector tidak berjalan terus-menerus; itu memboroskan tenaga. Ia menunggu sampai cukup banyak sampah baru
menumpuk, dan GOGC (default 100) menentukan seberapa banyak:
// target heap = live × (1 + GOGC/100)
// GOGC=100, live=10GB → target = 20GB
// GC jalan begitu 10GB lagi sudah dialokasi.
Jadi kalimat populer itu benar: kalau memori live saya 10GB, Go menunggu sampai 10GB lagi dialokasi sebelum membersihkan. Dua catatan kaki menjaga supaya tetap akurat. "Live" berarti heapⓘ yang ditandai terjangkau di akhir siklus terakhir — sebuah baseline yang bergeser tiap siklus, bukan angka tetap. Dan sejak Go 1.19 itu bukan satu-satunya pemicu.
GOGC mengatur jarak antar-pembersihan, diukur sebagai persentase dari yang masih hidup.
Tombolnya adalah pengatur antara dua biaya
Anda tidak bisa meminimalkan memori dan CPU sekaligus — tombol ini menukar yang satu dengan yang lain:
GOGCrendah (mis. 50) — sering bersih. Heap tetap rapi (memori lebih kecil) tapi si pembersih jalan terus (CPU lebih besar).GOGCtinggi (mis. 200) — jarang bersih. Si pembersih nyaris menganggur (CPU lebih kecil) tapi heap tinggi (memori lebih besar).GOGC=off— tak pernah membersihkan saat tumbuh; heap naik sampai membentur batas.
Buku catatan dan gudang — stack vs heap
Kenapa si pembersih ada sama sekali? Karena soal di mana data bisa tinggal. Ada dua tempat, dan keduanya sangat berbeda.
Stackⓘ adalah buku catatan seorang pelayan. Sebuah fungsi menulis variabel lokalnya di halaman teratas, dan ketika fungsi itu selesai, halaman itu disobek dan dibuang — seketika, berurutan, tanpa perlu berpikir. Tiap goroutine membawa buku catatannya sendiri. Tak ada yang perlu mencari tahu mana sampah: posisinya sudah jelas.
Heap adalah gudang bersama. Siapa pun boleh menyimpan sesuatu, dan benda itu bertahan selama dibutuhkan — terpakai habis tanpa urutan tertentu. Karena Anda tak bisa sekadar "menyobek yang teratas", seseorang harus menyusuri rak dan bertanya "masih ada yang bisa menjangkau ini?" Seseorang itu adalah si pembersih — garbage collector. Gudang itulah satu-satunya alasan si pembersih ada.
Compiler-lah yang memutuskan ke tempat mana tiap nilai pergi, lewat escape analysisⓘ. Nilai yang tak hidup lebih lama dari fungsinya tinggal di buku catatan; yang lolos pergi ke gudang:
func onStack() int {
p := &Point{X: 1, Y: 2} // pointer lokal, tak pernah keluar → STACK, gratis
return p.X
}
func onHeap() *Point {
return &Point{X: 1, Y: 2} // dikembalikan → lolos → HEAP, si pembersih melacaknya
}
Keduanya berasal dari page 4KB
Baik buku catatan maupun gudang tak bicara langsung ke sistem operasi. OS membagikan memori hanya dalam satu unit — page 4KB — dan Anda cuma bisa mengambil atau mengembalikan satu page utuh, tak pernah separuh.
Yang berbeda adalah bagaimana tiap sisi memakai page itu. Stack memakainya langsung: stack tiap goroutine adalah deret berurutan page yang kecil (mulai ~8KB — dua page). Saat kurang, Go memetakan deret yang lebih besar, menyalin stack ke sana, lalu membebaskan page lama — murah, otomatis, tanpa fragmentasi. Heap memakainya grosiran: meminta ke OS tiap kali itu syscall yang lambat, jadi Go membeli satu arena 64MB (≈16.384 page) sekaligus lalu mengirisnya jadi span berisi slot seukuran — itulah kenapa kebanyakan alokasi tak pernah menyentuh kernel.
Stack — buku catatan
- Deret page berurutan, ditumbuhkan dengan menyalin ke deret lebih besar.
- Page dibebaskan begitu goroutine selesai — satu blok rapi, tanpa lubang.
- Tanpa arena, tanpa GC: pemakaian page persis mengikuti yang hidup.
Heap — gudang
- Page dibeli grosiran sebagai arena 64MB, diiris jadi span → slot.
- Page baru bebas saat semua objek di atasnya mati — satu yang bertahan mengunci seluruh page 4KB.
- Page yang dibebaskan dikembalikan ke OS dengan malas, jadi RSS tertinggal dari pemakaian nyata (biasanya wajar, bukan kebocoran).
Poin terakhir itu kejutan klasiknya: setelah si pembersih membebaskan objek, Go sering menahan page-nya —
mengembalikannya lambat — dan baru menyerahkannya saat ada tekanan, jadi top bisa menunjukkan memori tinggi
lama setelah heap menyusut.
Kapan mengambil *
Inilah jebakannya. Intuisi "pointer lebih kecil, jadi mengoper *MyStruct menghemat memori" itu terbalik di
Go. Pointer tak membuat struct lenyap — struct-nya tetap tinggal di suatu tempat. Dan mengambil & sering
memaksa tempat itu menjadi heap, justru hal mahal yang menjadi tugas seluruh GC:
func makeValue() Point { return Point{} } // bisa tinggal di STACK — tanpa GC
func makePtr() *Point { return &Point{} } // lolos → HEAP — si pembersih menandainya terus
Menyalin struct kecil di stack praktis gratis. Alokasi heap tidak — dan lebih buruk, biayanya berulang: si
pembersih menandai ulang objek itu di tiap siklus sampai tak terjangkau. Jadi * bukan optimasi memori.
Ia alat semantik — untuk berbagi dan mutasi — yang kebetulan mendorong data ke heap.
Aturan yang mengalahkan default condong-ke-nilai — langgar ini dan Anda dapat bug, bukan sekadar kode lebih lambat:
Pakai pointer saat
- Anda harus memutasi aslinya (receiver/param nilai dapat salinan; perubahan Anda lenyap).
- Struct memuat field anti-salin —
sync.Mutex,WaitGroup, atauatomic.*(go vetmenandai penyalinan). nilharus jadi keadaan sah ("tidak ada"), seperti pada(*T, error).- Struct cukup besar sampai menyalinnya lebih mahal dari satu alokasi heap + pindai GC.
Pilih nilai saat
- Kecil dan hanya-baca — parameter nilai yang tak lolos tak pernah menyentuh heap.
- Anda mengembalikan struct kecil/sedang yang baru —
return T{}bisa tinggal di stack pemanggil;return &T{}memaksa heap. - Tak ada aturan pointer di atas yang berlaku. Maka nilai menjaga data di buku catatan, dan si pembersih tak pernah melihatnya.
Jangan menebak ke mana sesuatu mendarat — tanya compiler-nya. Escape analysis bisa dicetak:
go build -gcflags='-m' ./...
# ./main.go:12:10: &Point{} escapes to heap
# ./main.go:6:9: &Point{} does not escape
Tiap baris escapes to heap adalah tekanan GC yang bisa Anda lihat — dan sering versi nilai mengalokasi nol.
Seluruh rantai ini berpulang pada satu aturan: simpan data di buku catatan selama bisa; pakai gudang hanya saat harus. Objek termurah yang pernah diurus si pembersih adalah yang tak pernah Anda taruh di heap.
Sources