Catatan dari produksi
GoGarbage CollectionMemory

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.

MARKlive=10GBALLOCATEheap growssweep?SWEEPfree deadyesnew live_heap → next baseline (every cycle)heap ≥ 20GB || heap ≥ GOMEMLIMITtrue → run the sweep · false → keep allocating
Loopnya: tandai live, alokasi, lalu sapu hanya saat heap ≥ target GOGC ATAU heap ≥ GOMEMLIMIT — selain itu terus alokasi. Setelah disapu, heap live baru jadi baseline berikutnya.

GOGC mengatur jarak antar-pembersihan, diukur sebagai persentase dari yang masih hidup.

model mental satu baris

Tombolnya adalah pengatur antara dua biaya

Anda tidak bisa meminimalkan memori dan CPU sekaligus — tombol ini menukar yang satu dengan yang lain:

GOMEMLIMITGOGC=50@15GB · GC oftenGOGC=100@20GB · balancedGOGC=200@30GB · GC rareliveheadroom (GOGC)
Tombol menggeser pita ruang kosong. Live (blok pejal) tetap; hanya ruang kosong yang berubah. GOMEMLIMIT adalah plafon yang memaksa pembersihan lebih awal.
  • GOGC rendah (mis. 50) — sering bersih. Heap tetap rapi (memori lebih kecil) tapi si pembersih jalan terus (CPU lebih besar).
  • GOGC tinggi (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.

STACK · the notepadframe: localstorn off on return →
Stack adalah buku catatan: tiap frame fungsi adalah satu halaman, disobek begitu fungsi selesai — tanpa perlu kru pembersih.

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.

HEAP · the workstationyou · the cookcleaner · GCyou pile it on · the cleaner clears the unused
Heap adalah meja kerja si juru masak: Anda menumpuk objek sambil bekerja, dan si pembersih (GC) menyingkirkan apa pun yang sudah selesai agar meja tetap bisa dipakai.

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.

STACKthe notepad4KB4KB4KB4KB4KB4KBfrom the OS — a page at a timeHEAPthe pantry64MB arena64MBfrom the OS — one wholesale arena
Stack dan heap, dan dari mana tiap sisi dapat memori: OS menyuapi stack page 4KB satu per satu (lihat satu naik ke tiap frame), dan menyerahkan heap satu arena 64MB grosiran — keduanya diukir dari RAM 4KB yang sama.

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.

4KB pageOS unitSTACK4KB4KB4KBcontiguous run · grows by copying · no arenaHEAP64MB ARENA≈ 16,384 × 4KB pagesspans→ object slotsGo buys pages wholesale, then resells slots
OS hanya berurusan dengan page 4KB. Stack mengambil deret berurutannya; heap membelinya grosiran sebagai arena 64MB, lalu mengiris tiap arena jadi span berisi slot objek.

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.

&MyStruct{}take a pointerescapes?noSTACKfree · no GCyesHEAPGC marks it
Escape analysis, bukan simbol *, yang menentukan rumahnya. Pointer yang tak lolos tetap di stack; nilai yang lolos tetap mendarat di 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-salinsync.Mutex, WaitGroup, atau atomic.* (go vet menandai penyalinan).
  • nil harus 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

  1. A Guide to the Go Garbage Collector — go.dev
  2. runtime — GOGC & GOMEMLIMIT (dokumentasi paket)
  3. Getting to Go: the journey of Go's garbage collector