Tri-color Mark-Sweep GC, Write Barriers, and GOGC Pacing
- Last updated
- Prerequisites:
- Go memory model basics
- Understanding of GC concepts (mark, sweep)
- go
- garbage-collector
- tri-color
- write-barrier
- gogc
- memory
Read at your depth
The practical view
Go's GC is a concurrent, tri-color mark-sweep collector. Colors: white (unvisited), grey (found but not yet scanned), black (fully scanned). GC runs concurrently with your program, with brief stop-the-world pauses for phase transitions. It's triggered by allocation pressure: the GOGC environment variable (default 100) sets the target heap growth — GC runs when the heap grows ~100% since the last cycle. Tune with GOGC=200 (less frequent, more memory) or GOGC=50 (more frequent, less memory), or set debug.SetGCPercent. Modern Go also has a soft memory limit via GOMEMLIMIT.
The same idea in other frameworks
N/A — Go runtime concern.N/A — Go runtime concern.N/A — Go runtime concern.N/A — Go runtime concern.Legacy vs modern
Default GOGC vs tuned GOGC for a latency-sensitive service
Default GC (GOGC=100) balances CPU vs memory; a lower GOGC keeps memory down at the cost of more GC cycles.
// ❌ OLD: GOGC=100 default — GC pacing may cause latency spikes// ✅ NEW: soft memory limit + explicit pacing
// GOMEMLIMIT=512MiB GOGC=200 ./appInterview gotchas
Context
This tests the concurrent-GC correctness mechanism — interviewers want the tri-color invariant and why the barrier exists, not just 'it's concurrent'.
The mechanical answer
During concurrent marking, the mutator (your program) continues running and can modify the object graph. The tri-color invariant requires that no BLACK (fully-scanned) object holds a pointer to a WHITE (unmarked) object — otherwise the GC could finish marking, consider the white object garbage, and free it while a live black object still points to it. The write barrier intercepts pointer stores: when a goroutine writes a pointer, the barrier records the write (in Go's case, the Dijkstra insert barrier marks the newly-referenced object grey, or the Yuasa deletion barrier re-scans during the scan phase). This guarantees the GC never frees a reachable object. Without the barrier, the GC would need to stop the world for the entire mark, destroying concurrency.
Trap
The naive answer is 'the write barrier makes GC faster'. It's a correctness mechanism for concurrent marking, not a speed optimization (it adds a small per-pointer-store cost). Another trap: assuming the heap is compacted — Go's GC is non-moving, so pointers stay valid and the barrier approach is safe. Candidates who state the tri-color invariant and the insert-barrier behavior show real GC understanding.
Context
This tests the pacing model — interviewers want the heap-growth trigger and how the memory limit overrides/adjusts it, which is the modern Go tuning story.
The mechanical answer
GOGC (default 100) sets a percentage: the GC triggers when the estimated live heap grows by that percentage since the end of the last cycle. So GOGC=100 means 'collect when the heap has roughly doubled in size'. This gives a fixed CPU/memory trade-off (roughly 2% CPU overhead target) but can let memory spike on bursty allocation. GOMEMLIMIT (set via GOMEMLIMIT env or debug.SetMemoryLimit) is a soft memory limit: the GC controller incorporates the limit into its pacing, triggering collection earlier as the heap approaches the limit so total memory stays bounded. The two interact: the GC uses whichever trigger comes first, and the limit is soft (the runtime may exceed it under allocation pressure rather than risk live-set failure, but it aggressively collects to return).
Trap
The naive answer is 'GOMEMLIMIT is a hard cap'. It's a soft limit — the runtime can exceed it briefly rather than crash, but it will collect hard to get back under. Another trap: thinking GOGC is a CPU limit — it's a heap-growth percentage; actual CPU used depends on live-set size and allocation rate. Candidates who describe the trigger math and the soft-limit behavior show real depth.