What does GOGC control, exactly? What happens with GOGC=off, 50 or 200?
GOGC sets the heap growth target relative to the live data left after the last collection. Since Go 1.18 the formula also counts GC roots:
heapGoal = liveHeap + (liveHeap + stacks + globals) * GOGC/100
With the default GOGC=100 and 100 MB live, the next cycle starts at roughly 200 MB total heap. The pacer starts marking before the goal is reached, so marking finishes right about at the goal.
GOGC=50: collects twice as often and uses less memory, at the cost of more GC CPU.GOGC=200: collects about half as often and uses more memory, which means better throughput.GOGC=off: never collects unless a memory limit is set. Useful for short CLI tools, or together with GOMEMLIMIT.
import "runtime/debug"
func init() {
old := debug.SetGCPercent(200) // same as GOGC=200; returns previous value
_ = old
}
Key insight: GC CPU cost scales with how often the collector runs multiplied by live heap size. Garbage itself costs nothing to collect, since sweeping is cheap. So doubling GOGC roughly halves GC CPU. That is the classic trade between memory and CPU.
Gotcha: with a tiny live heap, a GOGC-only setup triggers GC constantly, because the minimum heap goal is only 4 MB. This is the "Twitch ballast" problem that GOMEMLIMIT now solves.
More on Memory, GC & Runtime Internals
- Q338Walk through the phases of a GC cycle. Where are the stop-the-world pauses?
- Q339Is Go's GC generational or compacting? What is the "Green Tea" GC?
- Q341What is GOMEMLIMIT and how would you set it in a container?
- Q342How do you read a line of
GODEBUG=gctrace=1output? - Q343Why is the process RSS much larger than the heap in use? How does Go return memory to the OS?
- Q344What techniques do you use to reduce allocations in a hot path?