Skip to content

Garbage Collection#

A garbage collector reclaims memory the program no longer references. Different algorithms trade pause time, throughput, and memory overhead.

Animation of the naive mark and sweep garbage collection algorithm marking reachable objects then sweeping the rest
Source: Wikimedia Commons. CC BY-SA.
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.Pool in 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.

Further reading#