# Go GC - Garbage Collector Internals
## Overview
Go uses a **concurrent, tri-color mark-and-sweep** garbage collector. Its design prioritizes low latency (short stop-the-world pauses) over raw throughput, making it well-suited for server workloads.
## The Collection Algorithm
### Tri-Color Mark-and-Sweep
The GC works in two logical phases: **mark** and **sweep**.
During the **mark phase**, the runtime traverses all objects reachable from _roots_ — goroutine stacks, global variables, and heap pointers held by the runtime. Every reachable object is marked as live. The algorithm uses three conceptual colours to track state:
|Colour|Meaning|
|---|---|
|**White**|Not yet visited — assumed dead|
|**Grey**|Discovered, but children not yet scanned|
|**Black**|Fully scanned; all children are also reachable|
At the end of the mark phase, anything still **white** is unreachable garbage.
During the **sweep phase**, white (dead) objects are freed and their memory is returned to the heap allocator for future use.
### Concurrent Execution
The key property of Go's GC is that it runs **concurrently** with your program. Most marking work happens while goroutines are running, rather than stopping the world. Stop-the-world pauses exist but are kept very short (typically sub-millisecond) — they occur at the start of the mark phase to scan roots and at the end to finalise marking.
This design means GC overhead is usually low even when collections are frequent.
## Core Concepts
### The Live Heap
The **live heap** is the amount of heap memory occupied by reachable objects _after_ a GC cycle completes. It represents what your program genuinely needs to function right now.
```
Before GC: [live: 60 MB] + [dead: 60 MB] = 120 MB total heap
After GC: [live: 60 MB] = 60 MB total heap
```
Short-lived allocations — temporary buffers, request-scoped data, intermediate slices — inflate the heap between collections but are not part of the live set. They will be dead by the time the next GC runs.
The live heap is the **baseline** that drives the GC pacing algorithm.
## Pacing and Tuning
### GOGC — The Primary Trigger
`GOGC` controls _when_ the next GC cycle triggers. Its value is a percentage representing how much the heap is allowed to grow beyond the live heap before GC runs again.
```
GC trigger = live_heap_after_last_GC × (1 + GOGC/100)
```
The default is `GOGC=100`, meaning GC runs when the heap doubles.
|`GOGC` value|Heap growth allowed|Trigger (60 MB live heap)|
|---|---|---|
|`100` (default)|2×|~120 MB|
|`200`|3×|~180 MB|
|`400`|5×|~300 MB|
|`800`|9×|~540 MB|
**This is a ratio, not an absolute size.** It doesn't care about available system RAM or any memory limit you've set — it's purely relative to the last-observed live heap.
Increasing `GOGC` reduces GC _frequency_ at the cost of higher peak memory usage. Decreasing it reduces peak memory at the cost of more frequent collections.
### GOMEMLIMIT — The Safety Ceiling
`GOMEMLIMIT` is a hard cap on total Go runtime memory usage. It is not a pacing mechanism — it's a **guard rail**.
When the heap approaches `GOMEMLIMIT`, the runtime will force GC regardless of what `GOGC` says, to avoid an OOM kill. Under normal operating conditions, `GOMEMLIMIT` should never come into play.
|Setting|Role|Analogy|
|---|---|---|
|`GOGC`|Controls how _often_ GC runs|Cruise control|
|`GOMEMLIMIT`|Caps _total_ memory usage|Cliff edge|
A common pattern is to set `GOMEMLIMIT` to a safe fraction of available container memory, and tune `GOGC` independently for the desired GC frequency trade-off.
## The GC Cycle — Visualised
```
Heap size
^
120 MB | /\ /\ /\
| / \ / \ / \
60 MB |______/ \____/ \____/ \___ ← live heap (floor after GC)
|
+-----------------------------------------> time
↑ ↑ ↑
GC runs GC runs GC runs
```
Each peak occurs when the heap hits the `GOGC` threshold. The GC sweeps the dead objects, the heap drops back to the live baseline, and the cycle repeats. With a high allocation rate, cycles can be frequent even though each individual collection is fast.
## What GC Tuning Can and Cannot Fix
### What GOGC tuning helps with
- Reducing GC **frequency** (at the cost of higher peak memory)
- Reducing GC-related CPU overhead when the GC itself is the bottleneck (visible as GC mark/sweep time in profiles)
- Smoothing out latency spikes caused by GC pause activity
### What GOGC tuning cannot fix
The GC pacing only controls _when_ objects are collected — it has no effect on the cost of _creating_ them. If profiling reveals that a significant portion of CPU time is spent in `runtime.mallocgc` (heap allocation) or `runtime.newstack`/`runtime.copystack` (goroutine stack growth), the underlying cause is a high **allocation rate**, not GC collection overhead.
In these cases, objects still need to be allocated at the same rate regardless of when they're swept. The relevant levers are:
- **`sync.Pool`** — reuse short-lived objects across goroutines to avoid repeated allocation
- **Pre-allocation** — size slices and maps at construction time to avoid repeated growth copies
- **Escape analysis** — keep objects on the stack where possible (`go build -gcflags="-m"` shows what escapes)
- **Reducing goroutine churn** — limit creation of short-lived goroutines; use worker pools
## Quick Reference
```bash
# View current GC settings
GODEBUG=gctrace=1 ./your-binary
# Set GC target (default 100)
GOGC=200 ./your-binary
# Set memory limit (Go 1.19+)
GOMEMLIMIT=512MiB ./your-binary
# Disable GC entirely (for benchmarks only)
GOGC=off ./your-binary
```
```go
// Programmatic control (runtime/debug package)
import "runtime/debug"
debug.SetGCPercent(200) // equivalent to GOGC=200
debug.SetMemoryLimit(512 << 20) // equivalent to GOMEMLIMIT=512MiB
```
---
## See Also
- [Go GC Guide (official)](https://tip.golang.org/doc/gc-guide)
- [runtime/debug package](https://pkg.go.dev/runtime/debug)
- `go tool pprof` — heap and CPU profiling
- `GODEBUG=gctrace=1` — real-time GC trace output