Garbage Collection

advanced go runtime

Garbage collection (GC) is the runtime automatically finding memory we’re no longer using and freeing it. We never call free() in Go — the collector does it for us.

In simple language: we keep allocating stuff, and a little janitor runs in the background, figures out what’s trash, and cleans it up.

Why Go’s GC is special

Most GCs make you pick: fast cleanup (throughput) OR short pauses (latency). Go picked latency.

The whole design goal is: don’t freeze the program for long. Pauses are tiny — usually sub-millisecond. That’s perfect for servers handling live requests, where a 100ms freeze means angry users.

The trade-off? Go’s GC does a bit more total work and uses more CPU than a throughput-focused collector. Go’s authors decided short, predictable pauses are worth it.

The three big design choices

Go’s collector is:

  • Concurrent — it runs at the same time as our program, on separate goroutines. It barely stops the world.
  • Non-generational — it doesn’t sort objects into “young” and “old” buckets like Java does. It just scans everything reachable.
  • Non-compacting — it never moves objects around to defragment memory. Once a thing has an address, it stays put. (This is partly why Go pointers are safe to hold.)

The tri-color mark-and-sweep — explained simply

This is the heart of it. The collector splits all objects into three colors:

  • White = “probably garbage, haven’t looked at it yet.”
  • Grey = “reachable, but I still need to check what it points to.”
  • Black = “reachable, and I’ve already checked everything it points to. Done.”

In simple language: white is the suspect pile, grey is the to-do list, black is the verified-safe pile.

The algorithm:

  1. Everything starts white.
  2. Mark the roots (global variables, things on goroutine stacks) grey.
  3. Pick a grey object, look at everything it points to, paint those grey, then paint the object itself black.
  4. Repeat until no grey objects remain.
  5. Whatever is still white is unreachable → that’s garbage → sweep (free) it.
WHITE
Not scanned yet. Suspect — might be garbage.
GREY
Reachable, but its children still need checking. The to-do list.
BLACK
Reachable AND fully scanned. Safe — keep it.
Start all WHITE → roots go GREY → scan grey, children turn GREY, scanned turns BLACK → leftover WHITE = garbage → sweep

How does it scan while the program is still running?

Here’s the tricky part. If the GC is marking objects while our code keeps changing pointers, our code could sneak a new pointer from a black (done) object to a white (about-to-be-deleted) object. The collector would never see it and would wrongly free live memory. Disaster.

The fix is write barriers.

In simple language: a write barrier is a tiny piece of code the compiler injects around pointer writes. While the GC is running, whenever we change a pointer, the barrier whispers to the collector “hey, note this new connection.” This keeps the colors honest so nothing reachable gets thrown away.

There are two brief stop-the-world (STW) pauses — to turn the write barrier on at the start and to finish marking at the end — but these are the sub-millisecond pauses, not a full freeze.

Tuning knob #1: GOGC

GOGC controls how often the GC runs. The default is 100.

It means: let the heap grow to 100% bigger than the live set before collecting again. So if 50MB is live, GC triggers around 100MB.

  • Higher GOGC (e.g. 200) = GC runs less often = more memory used, less CPU spent collecting.
  • Lower GOGC (e.g. 50) = GC runs more often = less memory, more CPU.
GOGC=200 ./myserver   # trade memory for fewer GC cycles (lower CPU)
GOGC=off ./myserver    # disable GC entirely (rare — only short-lived tools)

Tuning knob #2: GOMEMLIMIT (Go 1.19+)

GOGC alone has a flaw: if your live memory suddenly spikes, the heap target spikes too, and you can run out of RAM and get OOM-killed.

GOMEMLIMIT is a soft memory limit. We tell Go “try hard not to exceed this much memory total.” As we approach the limit, the GC runs more aggressively to stay under it.

GOMEMLIMIT=512MiB ./myserver   # soft cap; GC works harder near 512MiB

In simple language: GOGC is the normal pace, GOMEMLIMIT is the emergency brake. The common combo for containers is to set GOMEMLIMIT near your container memory and let GOGC handle the normal rhythm. It’s “soft” because Go will blow past it rather than crash if memory is genuinely needed.

Interview soundbite

Go uses a concurrent, non-generational, non-compacting tri-color mark-and-sweep collector tuned for low latency — pauses are typically sub-millisecond because marking runs alongside our program, guarded by write barriers so live objects never get freed mid-scan. We tune it with GOGC (how much the heap grows before collecting, default 100) and the soft GOMEMLIMIT (Go 1.19+) to keep memory under a cap, which together prevent OOM kills in containers.