Garbage collector Go bekerja otomatis dan cukup cerdas, tetapi seberapa sering ia bekerja bukan keputusan yang selalu optimal untuk semua aplikasi. Program batch yang ingin throughput maksimal rela memakai memori lebih besar; layanan interaktif butuh heap ramping agar latensi stabil; aplikasi di container tidak boleh melewati batas memori pod. Tiga tuas resmi mengatur semua itu: GOGC, GOMEMLIMIT, dan pemanggilan runtime.GC() manual — dan chapter ini membuktikan efeknya lewat penghitung siklus NumGC.
Cara Kerja Singkat GC Go #
GC Go menandai nilai yang masih hidup lalu menyapu sisanya, berjalan paralel dengan program (concurrent). Ritmenya ditentukan heap target: ketika alokasi baru membuat heap mendekati target, siklus berjalan. Dengan GOGC=100 (nilai bawaan), target dihitung sebagai heap hidup dikali dua — GC menyediakan ruang tumbuh 100% sebelum siklus berikutnya. Mekanisme lengkapnya dibahas pada chapter Garbage Collection
; untuk penalaan, dua angka itulah yang perlu kita pahami.
GOGC: Persentase Pertumbuhan Heap #
GOGC mengatur seberapa besar ruang tumbuh yang disediakan. Naikkan ke 400 → target membesar → siklus jarang → throughput naik, tetapi RSS membesar. Turunkan ke 50 → siklus sering → heap ramping, tetapi CPU terpakai untuk GC. Setel lewat environment variable GOGC=400 untuk seluruh proses, atau lewat debug.SetGCPercent(400) di dalam program — keduanya mengatur hal yang sama; angka -1 mematikan GC otomatis sama sekali.
Demo: Mematikan GC, Memaksa Siklus, dan Limit Lunak #
Berkas gc-demo.go membuktikan tiap tuas lewat NumGC — penghitung siklus GC yang selesai sejak program start. runtime.GC() tetap berjalan meski GC otomatis dimatikan, dan limit memori lunak tetap memicu siklus ketika terlampaui.
package main
import (
"fmt"
"math"
"runtime"
"runtime/debug"
)
// sink membuat alokasi pasti terjadi: hasil make disimpan ke variabel
// global sehingga compiler tidak dapat menghapusnya.
var sink []byte
// alokasiSampah mengalokasikan totalMB memori berulang kali; potongan
// lama tertimpa dan menjadi sampah yang harus dibersihkan GC.
func alokasiSampah(totalMB int) {
chunk := 256 << 10 // 256 KiB
for diberikan := 0; diberikan < totalMB<<20; diberikan += chunk {
sink = make([]byte, chunk)
}
}
// jumlahSiklusGC membaca NumGC: jumlah siklus GC yang selesai sejak
// program start, penghitung kumulatif yang naik monoton.
func jumlahSiklusGC() uint32 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.NumGC
}
func main() {
// Bagian 1: matikan GC otomatis, buktikan siklus tidak bertambah.
gogcAwal := debug.SetGCPercent(-1)
fmt.Println("GOGC awal yang dikembalikan SetGCPercent:", gogcAwal)
terakhir := jumlahSiklusGC()
alokasiSampah(64)
fmt.Println("Alokasi 64 MB sampah dengan GC mati : siklus bertambah", jumlahSiklusGC()-terakhir)
// Bagian 2: runtime.GC() memaksa siklus berjalan meski GC otomatis mati.
terakhir = jumlahSiklusGC()
runtime.GC()
runtime.GC()
runtime.GC()
fmt.Println("Tiga panggilan runtime.GC() paksa : siklus bertambah", jumlahSiklusGC()-terakhir)
// Bagian 3: aktifkan GC kembali, lalu pasang limit memori lunak.
debug.SetGCPercent(gogcAwal)
limitLama := debug.SetMemoryLimit(8 << 20)
fmt.Println("Limit memori awal tidak terbatas :", limitLama == math.MaxInt64)
terakhir = jumlahSiklusGC()
alokasiSampah(32)
berjalan := jumlahSiklusGC() > terakhir
fmt.Println("Alokasi 32 MB di bawah limit 8 MB : GC tetap berjalan", berjalan)
debug.SetMemoryLimit(limitLama)
fmt.Println("Program selesai; limit lunak tidak mematikan proses")
}Jalankan dari folder chapter:
go run gc-demo.goOutput yang Dihasilkan:
GOGC awal yang dikembalikan SetGCPercent: 100
Alokasi 64 MB sampah dengan GC mati : siklus bertambah 0
Tiga panggilan runtime.GC() paksa : siklus bertambah 3
Limit memori awal tidak terbatas : true
Alokasi 32 MB di bawah limit 8 MB : GC tetap berjalan true
Program selesai; limit lunak tidak mematikan prosesMari kita telaah baris demi baris program di atas:
- Baris 16-21 (
func alokasiSampah): mengalokasikan potongan 256 KiB berulang kali; potongan lama tertimpa sehingga menjadi sampah.sinkglobal memastikan alokasi tidak dihapus compiler. - Baris 25-29 (
func jumlahSiklusGC): membacaNumGCdariReadMemStats— penghitung kumulatif yang naik tiap siklus selesai. - Baris 33-38:
SetGCPercent(-1)mematikan GC otomatis dan mengembalikan nilai sebelumnya (100); 64 MB sampah dialokasikan tanpa satu siklus pun berjalan. - Baris 41-45: tiga panggilan
runtime.GC()memaksa tiga siklus penuh — pemanggilan manual semacam ini hanya cocok untuk pengujian dan pengukuran, bukan kode produksi. - Baris 48-55: GC diaktifkan kembali,
SetMemoryLimit(8 << 20)memasang limit lunak 8 MB; alokasi 32 MB memicu siklus berulang, dan program tetap hidup — limit lunak membuat GC bekerja lebih keras, bukan mematikan proses.
GOMEMLIMIT dan Container #
GOMEMLIMIT (Go 1.19+) adalah limit memori lunak yang dikejar runtime: saat total memori yang dipakai runtime mendekati limit, GC berjalan lebih agresif — bahkan ketika GOGC mengizinkan heap lebih besar, dan tetap aktif saat GOGC=off. Nilai bawaannya math.MaxInt64 (praktis tak terbatas), yang dibuktikan baris limitLama == math.MaxInt64 pada output. Batas ini lunak: melewatinya membuat GC bekerja keras, bukan membuat program crash.
Ballast: Teknik Warisan #
Sebelum GOMEMLIMIT ada, trik ballast populer: mengalokasikan []byte sangat besar yang tidak pernah disentuh, sehingga heap terlihat besar dan GC jarang terpicu. Teknik ini kini warisan — GOMEMLIMIT mencapai efek yang sama secara resmi dan lebih presisi; proyek lama yang masih memakai ballast sebaiknya dimigrasikan.
Trade-off: Throughput, Latensi, Memori #
| Pengaturan | Efek | Cocok untuk |
|---|---|---|
GOGC naik (mis. 400) | Siklus jarang, throughput naik, RSS besar | Batch, ETL, RAM longgar |
GOGC turun (mis. 50) | Siklus sering, heap ramping | Layanan sensitif memori |
GOGC=off | GC otomatis mati | Hanya pengukuran terisolasi |
GOMEMLIMIT | GC mengejar target memori | Container, proteksi OOM |
Kombinasi yang umum untuk layanan di container: GOMEMLIMIT sebagai jaring pengaman ditambah GOGC yang disesuaikan profil — naikkan bila profil menunjukkan banyak waktu habis untuk GC dan memori masih tersedia.
Setelah ritme GC, lapisan terakhir yang menentukan kecepatan ada di compiler: inlining, bounds check, dan devirtualization bekerja diam-diam di balik kode — chapter berikutnya membaca buktinya langsung dari laporan compiler.
Referensi #
- Panduan GC Go — dokumen resmi penalaan GOGC dan GOMEMLIMIT.
- Package runtime/debug — SetGCPercent — API penalaan GOGC.
- Package runtime/debug — SetMemoryLimit — API limit memori lunak.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
gc-demo.go
Unduh berkaspackage main
import (
"fmt"
"math"
"runtime"
"runtime/debug"
)
// sink membuat alokasi pasti terjadi: hasil make disimpan ke variabel
// global sehingga compiler tidak dapat menghapusnya.
var sink []byte
// alokasiSampah mengalokasikan totalMB memori berulang kali; potongan
// lama tertimpa dan menjadi sampah yang harus dibersihkan GC.
func alokasiSampah(totalMB int) {
chunk := 256 << 10 // 256 KiB
for diberikan := 0; diberikan < totalMB<<20; diberikan += chunk {
sink = make([]byte, chunk)
}
}
// jumlahSiklusGC membaca NumGC: jumlah siklus GC yang selesai sejak
// program start, penghitung kumulatif yang naik monoton.
func jumlahSiklusGC() uint32 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.NumGC
}
func main() {
// Bagian 1: matikan GC otomatis, buktikan siklus tidak bertambah.
gogcAwal := debug.SetGCPercent(-1)
fmt.Println("GOGC awal yang dikembalikan SetGCPercent:", gogcAwal)
terakhir := jumlahSiklusGC()
alokasiSampah(64)
fmt.Println("Alokasi 64 MB sampah dengan GC mati : siklus bertambah", jumlahSiklusGC()-terakhir)
// Bagian 2: runtime.GC() memaksa siklus berjalan meski GC otomatis mati.
terakhir = jumlahSiklusGC()
runtime.GC()
runtime.GC()
runtime.GC()
fmt.Println("Tiga panggilan runtime.GC() paksa : siklus bertambah", jumlahSiklusGC()-terakhir)
// Bagian 3: aktifkan GC kembali, lalu pasang limit memori lunak.
debug.SetGCPercent(gogcAwal)
limitLama := debug.SetMemoryLimit(8 << 20)
fmt.Println("Limit memori awal tidak terbatas :", limitLama == math.MaxInt64)
terakhir = jumlahSiklusGC()
alokasiSampah(32)
berjalan := jumlahSiklusGC() > terakhir
fmt.Println("Alokasi 32 MB di bawah limit 8 MB : GC tetap berjalan", berjalan)
debug.SetMemoryLimit(limitLama)
fmt.Println("Program selesai; limit lunak tidak mematikan proses")
}