Garbage Collection#
A garbage collector reclaims memory the program no longer references. Different algorithms trade pause time, throughput, and memory overhead.
flowchart TB
Roots[GC roots<br/>stack, globals, registers] --> R1[reachable]
R1 --> R2[reachable]
R2 --> R3[reachable]
Unreach[unreachable] -. swept .-> Free[free memory]
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class Roots,R1,R2,R3 service;
class Unreach,Free datastore;
Modern collectors (ZGC, Shenandoah, Go's GC) pause for sub-millisecond windows on multi-gigabyte heaps. Pre-2010 collectors paused for seconds.
Every collector solves the same problem (find and free unreachable objects) but optimises different axes: pause time, throughput, memory overhead, and CPU cost.
Algorithms#
Mark-sweep#
flowchart TB
Start[Stop the world] --> Mark[Mark reachable from roots]
Mark --> Sweep[Sweep unreachable -> free list]
Sweep --> Resume[Resume mutator threads]
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class Start,Mark,Sweep,Resume service;
Simple; pauses proportional to live heap. Fragmentation accumulates over time.
Copying / Cheney#
Two semi-spaces: copy live objects from "from" to "to" space, then swap. Compacts as it goes. Uses 2× memory.
Mark-compact#
Mark, then compact live objects to one end of the heap. Eliminates fragmentation without 2× footprint.
Reference counting#
Each object tracks its incoming references; freed when count hits zero.
- Pros: deterministic, no pause; used in CPython, Swift, COM, Rust
Rc/Arc. - Cons: doesn't handle cycles (Python has a cycle collector as backup); per-write overhead.
Generational hypothesis#
Most objects die young.
So focus collection effort on the young generation:
flowchart TB
E[Eden] -->|survive minor GC| S0[Survivor 0]
S0 -->|survive next| S1[Survivor 1]
S1 -->|tenured| Old[Old generation]
Old -->|major GC, rarer| Old
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class E,S0,S1,Old datastore;
Minor GCs are frequent and fast (young gen, mostly garbage). Major GCs are rare and slow.
Concurrent and incremental#
Pauses become unacceptable past a few GB heap. Modern collectors do most work concurrently with mutator threads, pausing only briefly:
| Collector | Heap | Typical pause |
|---|---|---|
| G1 (JVM) | up to 100GB | 100-200ms |
| ZGC (JVM) | TB | < 1ms |
| Shenandoah (JVM) | TB | < 10ms |
| Go GC | TB | sub-ms |
| .NET Server GC | depends | configurable |
Concurrent collectors use read or write barriers: tiny code inserted on every reference operation so the collector can track changes the mutator makes during a concurrent mark.
GC pressure metrics#
| Metric | Meaning |
|---|---|
| Allocation rate | Bytes/sec; high → frequent GC |
| Promotion rate | Bytes/sec from young to old |
| Pause time (p99) | Tail latency added by GC |
| CPU% in GC | Throughput tax |
| Heap occupancy after GC | Live working set |
Tuning rules#
- Heap size: too small = constant GC; too big = long pauses (with old collectors). Modern concurrent GCs scale to large heaps.
- Allocate less: reuse buffers (
ByteBuffer,sync.Poolin Go), avoid creating wrapper objects in hot loops. - Right collector: G1 for throughput, ZGC/Shenandoah for tail-latency-sensitive workloads.
- Container-aware: JVM 11+ respects cgroup memory limits.
Without GC#
- C/C++: manual
malloc/free, RAII. - Rust: ownership + borrow checker eliminates need for GC.
- Arena allocators: bulk-free at end of request (good for short-lived workloads).
Common bugs#
- Memory leak via reference: long-lived collection holds short-lived objects.
- GC thrashing: heap too small; same data collected repeatedly.
- Allocation in hot loops: tail latency from young-gen collection.
- Finalizers: avoid; non-deterministic and slow.
Where GC fits#
flowchart TB
GC((Garbage<br/>collection))
IM[Immutability<br/>reduces GC pressure]
OP[Object pool pattern<br/>manual reuse alt]
CP[Concurrency primitives<br/>barriers in concurrent GC]
OBS[Observability<br/>GC metrics in dashboards]
IM -. less pressure .- GC
OP -. avoid GC .- GC
CP -. barriers .- GC
GC --> OBS
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class GC service;
class IM,OP,CP,OBS datastore;
Glossary & fundamentals#
| Tag | Concept | What it is | Page |
|---|---|---|---|
LLD |
Immutability | reduces GC pressure | immutability |
LLD |
Object pool pattern | manual reuse to dodge GC | object-pool-pattern |
LLD |
Concurrency primitives | barriers used in concurrent GC | concurrency-primitives |
HLD |
Observability | GC metrics in production | observability |
Quick reference#
Algorithms#
- Mark-sweep
- Copying / Cheney
- Mark-compact
- Reference counting (cycles need backup)
Generational hypothesis#
"Most objects die young" → focus on young generation. - Eden → Survivor → Tenured
Modern concurrent collectors#
| GC | Heap | Pause p99 |
|---|---|---|
| G1 (JVM) | up to 100GB | 100-200ms |
| ZGC | TB | < 1ms |
| Shenandoah | TB | < 10ms |
| Go GC | TB | sub-ms |
Barriers#
Concurrent GC uses read or write barriers to track changes the mutator makes during concurrent mark.
Tuning levers#
- Heap size (don't undersize)
- Allocation rate (reuse buffers)
- Collector choice (ZGC for latency, G1 for throughput)
- Container-aware flags (
-XX:+UseContainerSupport)
Metrics to watch#
- Allocation rate
- Promotion rate
- Pause time p99
- CPU% in GC
- Heap occupancy post-GC
Bugs#
- Long-lived map / list = leak
- GC thrashing (heap too small)
- Allocation in hot loops
- Finalizers (avoid)
Without GC#
- Rust ownership
- C/C++ RAII
- Arena allocators
Refs#
- Jones, Hosking, Moss - GC Handbook
- Oracle G1 docs
- Azul ZGC
- Go pacer blog
FAQ#
How does garbage collection actually work?#
A tracing collector starts from GC roots like the stack and globals, marks every object it can reach, then sweeps or compacts everything left unmarked as free memory.
What is a generational garbage collector?#
It splits the heap into young and old regions because most objects die young. Frequent cheap collections of the young generation catch most garbage with tiny pauses.
What causes stop-the-world GC pauses?#
Some collector phases need a consistent snapshot of references, so all application threads pause. Modern concurrent GCs like ZGC and Shenandoah keep these windows under a millisecond.
Reference counting versus tracing: which is better?#
Tracing handles cycles automatically and amortises work; reference counting is predictable but cannot collect cycles without extra machinery. Python and Swift use ref counting plus a cycle detector.
Can I make a GC application latency-friendly?#
Yes. Pick a low-pause collector, size the heap so collections are infrequent, avoid huge object allocations in hot paths, and pool reusable buffers to limit pressure.
Related Topics#
- Object Pool Pattern: the manual alternative when GC pressure matters
- Immutability: immutable objects are typically cheaper to collect
- Observability: GC metrics belong in every production dashboard