Garbage Collection in Go
The tricolor mark-sweep collector, write barriers, GC pacing, and triggering collection.
8 questions
JuniorTheoryVery commonWhat kind of garbage collector does Go use, and what are its defining properties?
What kind of garbage collector does Go use, and what are its defining properties?
Go uses a concurrent, tricolor mark-and-sweep collector. It is non-generational and non-compacting — objects never move, so pointers stay stable. Marking and sweeping run alongside the program, with only brief stop-the-world pauses.
Common mistakes
- ✗Calling Go's GC generational — Go has no young/old generations
- ✗Believing the collector compacts the heap and moves live objects
- ✗Confusing mark-and-sweep with reference counting
Follow-up questions
- →Why does a non-compacting collector risk heap fragmentation, and how does Go limit it?
- →What does 'tricolor' mean in Go's mark-and-sweep collector?
JuniorTheoryCommonWhat are stop-the-world pauses and when do they happen in Go's GC?
What are stop-the-world pauses and when do they happen in Go's GC?
A stop-the-world pause halts every goroutine so the runtime can act on a consistent heap. Modern Go has only two short STW pauses per cycle — at mark-start and mark-termination — each typically sub-millisecond. Marking and sweeping themselves run concurrently.
Common mistakes
- ✗Thinking Go stops the world for the whole marking phase, not just two short pauses
- ✗Believing STW pause length scales with the live heap size in modern Go
- ✗Assuming Go GC has zero pauses — it has brief STW pauses at cycle boundaries
Follow-up questions
- →Why is mark-termination still an STW pause if marking is concurrent?
- →How does the runtime stop every goroutine, given they aren't all at safepoints?
JuniorTheoryOccasionalHow is a GC cycle triggered, and what do GOGC and runtime.GC do?
How is a GC cycle triggered, and what do GOGC and runtime.GC do?
A cycle starts when the heap grows by GOGC percent since the previous cycle — at the default GOGC=100 the heap roughly doubles. runtime.GC() forces a cycle and blocks until it finishes. A forced cycle also runs if two minutes pass with no GC.
Common mistakes
- ✗Thinking
GOGCis a time interval rather than a heap-growth percentage - ✗Believing GC runs on a fixed timer instead of being allocation-driven
- ✗Assuming
runtime.GC()is asynchronous — it blocks until the cycle completes
Follow-up questions
- →How does
GOMEMLIMITchange when a cycle is triggered? - →What is the GC pacer and how does it decide when to start the next cycle?
MiddleTheoryOccasionalWhat does the GC pacer decide, and how does it make those decisions?
What does the GC pacer decide, and how does it make those decisions?
The pacer decides when to start the next cycle and how much mark-assist allocating goroutines must perform. It estimates allocation and marking rates so the concurrent mark finishes just as the heap reaches its GOGC-derived goal.
Common mistakes
- ✗Thinking the pacer controls collector thread count rather than cycle timing and assist
- ✗Believing the pacer reorders object scanning to speed up the cycle
- ✗Assuming the pacer's job is to cap STW pause duration
Follow-up questions
- →What happens to the pacer's behavior when
GOMEMLIMITis set near the live heap size? - →How does the pacer compute the per-goroutine assist credit
mark-assist debt?
MiddleTheoryOccasionalHow does the tricolor mark-sweep algorithm work in Go's collector?
How does the tricolor mark-sweep algorithm work in Go's collector?
Objects are white (candidate garbage), grey (reachable but unscanned), or black (reachable and scanned). Marking starts from roots, turns grey objects black while greying their referents, and ends when no grey remains. White objects are then swept.
Common mistakes
- ✗Reversing the colors — thinking black means garbage and white means live
- ✗Treating the colors as object ages rather than scan progress within one cycle
- ✗Forgetting the invariant that no black object may point to a white object
Follow-up questions
- →What is the tricolor invariant, and what breaks it during concurrent marking?
- →Why can a white object be freed safely once no grey objects remain?
MiddleTheoryOccasionalGo marks the heap concurrently while the program runs. Why does it need a write barrier to stay correct?
Go marks the heap concurrently while the program runs. Why does it need a write barrier to stay correct?
While marking runs, the mutator can store a pointer into an already-scanned black object, hiding a white object from the collector. The write barrier intercepts pointer writes and shades the affected object grey, preserving the invariant that no black object reaches a white one.
Common mistakes
- ✗Thinking the write barrier is a lock protecting color bits rather than a shading hook
- ✗Believing the barrier exists to relocate pointers — Go's collector never moves objects
- ✗Assuming concurrent marking is correct without any mutator cooperation
Follow-up questions
- →How does Go's hybrid write-barrier algorithm Dijkstra-Yuasa differ from a plain insertion barrier?
- →Why can the stack be left grey-free without a barrier on stack writes?
SeniorTheoryOccasionalHow does Go mark concurrently with the mutator, including the allocator throttle mark assist?
How does Go mark concurrently with the mutator, including the allocator throttle mark assist?
Background mark workers scan grey objects while the program runs; a write barrier keeps the tricolor invariant under concurrent pointer writes. If allocation outpaces marking, the pacer charges each allocating goroutine proportional mark-assist work.
Common mistakes
- ✗Confusing the write barrier with a read barrier — Go uses a write barrier
- ✗Thinking mark assist spawns workers rather than charging the allocating goroutine itself
- ✗Believing mark assist is an STW pause instead of inline work the mutator does
Follow-up questions
- →Why does charging mark assist to the allocator make the GC self-stabilizing?
- →How does the runtime split CPU between background mark workers and the mutator?
SeniorTheoryRareHow do you tune GC for latency versus throughput with GOMEMLIMIT and ballast?
How do you tune GC for latency versus throughput with GOMEMLIMIT and ballast?
Raising GOGC runs fewer cycles — more throughput, more memory; lowering it does the reverse. GOMEMLIMIT (Go 1.19+) sets a soft memory ceiling, collecting more aggressively as the heap nears it. The old ballast trick is now superseded by GOMEMLIMIT.
Common mistakes
- ✗Reversing
GOGCdirection — higherGOGCmeans fewer cycles, not more - ✗Treating
GOMEMLIMITas a hard cap that panics rather than a soft pacing target - ✗Still recommending the ballast trick instead of
GOMEMLIMITon modern Go
Follow-up questions
- →What happens if
GOMEMLIMITis set so low the live heap alone exceeds it? - →Why did ballast work before
GOMEMLIMIT, and what was its main drawback?