A learning path ready to make your own.

Garbage Collection Algorithms

Overview Garbage collection (GC) is automatic reclamation of memory no longer reachable by a program. GC designs balance tradeoffs among throughput (application CPU time), latency (pause times), memory footprint, fragmentation, and implementation complexity. Modern production GCs combine generational, concurrent, parallel and region-based techniques plus runtime machinery like barriers and remembered sets. Historical milestones 1960s: McCarthy’s Lisp memory management and early mark-and-sweep ideas. 1969–70: Cheney’s copying collector (non‑recursive compaction). 1970s–80s: Reference counting, cycle problems, and tri-color/incremental GC (Dijkstra et al.). 1990s–present: Generational, parallel and concurrent collectors for multicore and low-latency systems. 2010s–present: Region-based concurrent compactors (G1, Shenandoah, ZGC) for huge heaps and low pauses. Core concepts & terminology Heap: dynamic allocation region. Roots: registers, stacks, globals reachable by the program. Live/reachable: objects transitively reachable from roots. Tracing collector: mark reachable objects from roots. Reference counting (RC): per-object inbound reference counts. Compaction: moving objects to reduce fragmentation; requires updating references. Tri-color invariant & barriers: abstraction and write/read hooks used for correct concurrent/incremental GC. Tracing vs. Reference counting (summary) Tracing: handles cycles, can compact, lower per-object metadata; may cause stop-the-world pauses or require complex concurrent algorithms and barriers. Reference counting: immediate, incremental reclamation and deterministic deallocation; costs per-pointer-write updates, stores counts per object, and cannot collect cycles without extra mechanisms. Core algorithms (brief) Reference counting: increment/decrement counts on pointer writes; cascades free; low latency, cycle problem, write overhead. Mark-and-sweep: mark reachable objects then sweep unmarked ones; handles cycles, may fragment and cause pauses. Mark-and-compact: mark then move live objects to eliminate fragmentation; requires updating references. Copying (Cheney): semispace copying to a compact to-space; fast allocation and no fragmentation, but needs extra memory (or generational variants). Generational: partition by age (young/old); frequent cheap minor collections for short-lived objects; uses barriers/remembered sets for old→young pointers. Incremental/concurrent: overlap GC with mutator using tri-color invariants and barriers to reduce pause times at cost of runtime overhead. Parallel: use multiple cores to speed marking/copying/sweeping (often used with stop-the-world phases). Real-time: strict pause bounds via fine-grained scheduling of GC work; higher overhead. Region/arena allocation: bulk allocation/deallocation by freeing whole regions—very efficient for scoped lifetimes. Conservative GC: for C/C++ without pointer metadata; treats ambiguous words as potential pointers (may retain unreachable objects). Hybrid strategies: combine the above to leverage strengths and mitigate weaknesses (e.g., G1, Shenandoah, ZGC). Implementation techniques Root discovery & stack maps: precise GC uses metadata/safepoints; conservative scans are approximate. Tri-color & barriers: many concurrent collectors rely on write/read barriers (SATB, incremental-update) to preserve invariants. Card marking & remembered sets: track intergenerational or cross-region pointers efficiently during minor/evacuation collections. Large-object spaces & pinning: special treatment for humongous objects to avoid frequent copying; pinned objects complicate compaction. Finalizers & weak/soft/phantom refs: specialized reference types and lifecycle hooks with semantics for caches and cleanup. OS interaction: use of mmap, madvise, large pages, and NUMA-aware placement to manage VM behavior and performance. Real-world implementations Java HotSpot: Serial, Parallel, CMS (legacy), G1, Shenandoah, ZGC (region-based concurrent compactors). .NET CLR: generational, compacting, concurrent/server modes. Go: concurrent mark-and-sweep with write barriers and goroutine-aware safepoints. CPython: reference counting + cyclic detector. Boehm GC: conservative tracing for C/C++. V8, GHC, Swift/Objective-C ARC: tuned collectors or ARC tailored to language semantics. Metrics, tuning, and heuristics Key metrics: pause-time distributions, throughput, footprint, allocation speed, scalability, predictability. Tuning levers: nursery/tenured sizes, tenuring thresholds, GC thread count, promotion policies, allocation profiling. Tradeoffs: lower pause times often increase mutator overhead (barriers) or memory use; copying reduces fragmentation at memory cost. Limitations, safety & security GC reduces many memory-safety bugs (use-after-free), but finalizers, native interop, and pinned memory introduce complexity and potential leaks. Conservative GC can retain unreachable objects; real-time constraints may preclude general-purpose GC. Security and correctness require careful handling of native code, object pinning, and barrier correctness. Future directions Low-latency, large-heap collectors with pause times independent of heap size. Hardware-assisted barriers, tagged/pointer metadata, and specialized instructions to reduce runtime overhead. NVM/persistent-heap GC, distributed GC, and tighter integration with type/ownership systems. Optimizations for ML/AI workloads and ephemeral heavy-allocation patterns. Practical guidance Throughput batch jobs: parallel generational collectors. Latency-sensitive interactive services: concurrent region-based collectors (ZGC, Shenandoah) or tuned G1. Embedded or hard real-time: prefer region/arena allocation or specialized real-time collectors; avoid general-purpose GC when strict bounds are required. Systems/C/C++: use explicit pools, RC/ARC, or conservative GC where appropriate. Functional/high-allocation workloads: generational copying plus parallelism typically performs well. Summary GC design is a spectrum of tradeoffs: immediate deallocation (RC) vs global tracing (marking), pause-time vs throughput (concurrent vs stop-the-world), and memory overhead vs fragmentation (copying vs mark-sweep vs compacting). Modern production collectors are hybrid and highly engineered—combining generational hypotheses, concurrency, parallelism, barriers, and region-based heap layouts—to meet diverse application needs. Selecting or designing the right GC requires understanding allocation patterns, latency requirements, memory constraints, and platform characteristics. Further reading Classic papers: McCarthy (Lisp GC), Cheney (copying), Dijkstra et al. (tri-color/incremental), Baker (real‑time), Blackburn et al. (generational studies). Implementation papers and docs on G1, Shenandoah, ZGC, Go GC, and Boehm GC.

Follow the trail that experts already trust.

Resources

Read deeper, connect wider, own the subject.

Deep Article

Garbage collection (GC) is the automatic reclamation of memory that a program no longer needs. Modern GC algorithms are central to many managed languages (Java, C#, Go, JavaScript, Haskell, etc.), and they also appear in hybrid or optional forms (ARC/RC in Swift/Obj‑C, Boehm GC for C/C++). This article is a deep dive into garbage‑collection algorithms: history, fundamental concepts, core and advanced algorithms, implementation techniques, performance tradeoffs, use cases, and future directions.

Table of contents

  • Introduction and scope
  • Historical milestones
  • Fundamental concepts and terminology
  • Tracing vs reference counting: conceptual comparison
  • Core algorithms
  • Reference counting
  • Mark-and-sweep
  • Mark-and-compact
  • Copying collectors (Cheney)
  • Generational collection
  • Incremental and concurrent collectors
  • Parallel collectors
  • Real-time (hard real-time) collectors
  • Region-based and arena allocation
  • Conservative garbage collection
  • Hybrid strategies
  • Implementation details and engineering
  • Root set discovery and stack scanning
  • Tri-color invariant and barriers (write/read)
  • Card marking and remembered sets
  • Large object handling and humongous objects
  • Compaction, fragmentation, pinning
  • Finalizers, weak/soft/phantom references
  • Interaction with OS (virtual memory, page faults, NUMA)
  • Practical examples and pseudo-code
  • Simple reference counting
  • Mark-and-sweep (pseudocode)
  • Cheney’s copying (pseudocode)
  • Basic write-barrier examples
  • Real-world collectors and language implementations
  • Java HotSpot (Serial, Parallel, CMS, G1, Shenandoah, ZGC)
  • .NET CLR GC
  • Go runtime GC
  • CPython (reference counting + cycle collector)
  • Boehm conservative GC
  • V8 and JavaScript engines
  • Functional language GCs (GHC)
  • Swift/Obj‑C ARC
  • Metrics and evaluation: throughput vs latency vs footprint
  • Tuning strategies and heuristics
  • Limitations, safety, and security considerations
  • Future directions and research trends
  • Practical guidance: choosing/designing a GC for different workloads
  • Summary

Introduction and scope Garbage collection relieves programmers from managing memory explicitly (malloc/free, free()). A GC’s role is to detect unreachable objects and reclaim their storage while balancing tradeoffs: throughput (compute time dedicated to GC), latency (pause times/duration), memory footprint, fragmentation, and implementation complexity. This article covers classic algorithms, their modern variants (parallel, concurrent, generational), implementation techniques (barriers, remembered sets), and practical considerations for production systems.

Historical milestones

  • 1960s – John McCarthy proposed an automatic memory management scheme for Lisp; mark-and-sweep-like ideas appeared early.
  • 1969–1970 – Cheney’s algorithm for copying collection (nonrecursive compaction) introduced.
  • 1970s – Reference counting and cycle issues were studied and practical improvements proposed.
  • 1970s–1980s – Tri-color abstraction and on-the-fly/incremental GC (Dijkstra et al.) established formal invariants for concurrent collectors.
  • 1990s–present – Generational collection and parallel/concurrent collectors became mainstream for multi-core servers and low-latency workloads.
  • 2010s–present – Large-heap, low-latency collectors (G1, Shenandoah, ZGC) with concurrent compaction and region-based heaps.

Fundamental concepts and terminology

  • Heap: region of memory used for dynamic allocation.
  • Root set: references directly accessible from program state (global/static variables, CPU registers, stack frames, thread-local storage).
  • Live (reachable) objects: reachable from root set via chains of references. Anything else is garbage.
  • Tracing collector: identifies live objects by tracing from roots and marks reachable objects.
  • Reference counting: each object maintains a count of inbound references; count reaching zero triggers deallocation.
  • Mark-and-sweep: trace to mark live objects then sweep to reclaim unmarked objects.
  • Copying collector: moves live objects to a new space (evacuation), leaving behind compacted memory.
  • Compaction: moving objects to reduce fragmentation and improve locality.
  • Fragmentation: free memory scattered into small blocks; internal vs external fragmentation.
  • Pause time: time the mutator (application threads) is stopped for GC (stop-the-world).
  • Throughput: fraction of total time devoted to application vs GC.
  • Tri-color invariant: abstraction for incremental GC partitioning objects into white (unvisited), gray (discovered but children not processed), black (processed).
  • Barriers: code emitted on field reads/writes to maintain invariants in concurrent/incremental collectors.

Tracing vs reference counting: conceptual comparison

  • Tracing (mark-based): Pros: detects arbitrary cycles, can compact memory and reduce fragmentation, typically requires less per-object overhead. Cons: can require stop-the-world pauses or complicated concurrent algorithms and write-read barrier overhead.
  • Reference counting (RC): Pros: reclamation is immediate and incremental, simple to implement, low latency for single-threaded contexts. Cons: per-object metadata overhead (reference count), cost of updating counts on every pointer write, inability to reclaim cyclic structures without extra mechanisms, concurrency complexity.

Core algorithms

Reference counting Description:

  • Each object has a counter for number of references to it.
  • On assignment of a pointer, increment the new target’s count and decrement the old target’s count; when any decremented count hits zero, object is reclaimed, and its outgoing references are decremented recursively (cascading deallocation).

Pseudocode (simplified): `` function assign(slot, obj): old = slot.value if obj != null: obj.count += 1 slot.value = obj if old != null: old.count -= 1 if old.count == 0: finalizeandfree(old) ``

Pros:

  • Deterministic reclamation time; low-latency deallocations.
  • Good for real-time or incremental reclamation.

Cons:

  • Cannot collect cycles without cycle-detection algorithms (trial deletion, periodic cycle collector).
  • Per-write overhead: O(1) updates for each pointer write (can be expensive).
  • Concurrency requires atomic inc/dec or locks; contention on counts.

Practical uses:

  • CPython uses RC for immediate deallocation, supplemented by a cycle detector for container cycles.
  • Swift/Obj‑C use ARC (Automatic Reference Counting) which is compile-time assisted RC with runtime support.

Mark-and-sweep Description:

  • Stop-the-world (classically) tracing collector. Two phases: mark (trace from roots marking reachable objects); sweep (scan heap and reclaim unmarked objects).
  • Optionally followed by a compaction phase to eliminate fragmentation.

Pseudocode: ``` function collect(): markphase() sweepphase()

function mark_phase(): for root in roots: if not root.marked: mark(root)

function mark(obj): obj.marked = true for child in obj.fields: if not child.marked: mark(child)

function sweep_phase(): for each object in heap: if not object.marked: free(object) else: object.marked = false // reset for next GC ```

Pros:

  • Handles cycles naturally.
  • Low per-allocation overhead.

Cons:

  • Stop-the-world pauses (unless made incremental/concurrent).
  • Sweep can leave fragmentation unless compacted.

Mark-compact

  • Variation: after marking, relocate live objects to compact heap, then update references. Eliminates fragmentation but moving objects requires updating all references to moved objects (via forwarding pointers or two-pass updates, or using card-marking remembered sets to limit updates).

Copying collectors (Cheney’s algorithm) Description:

  • Divide heap into two semispaces: from-space and to-space. Allocate in from-space sequentially. When from-space fills, copy live objects to to-space by breadth-first forwarding, then swap spaces.
  • Cheney’s algorithm is non-recursive and uses a scanning pointer: copied objects are scanned to copy their children.

Pseudocode (high-level): ``` scan = to-space.start free = to-space.start

copyroot(root): if root.forwarding is set: return root.forwarding new = free copyobject(new, root) root.forwarding = new free += object_size return new

while scan < free: for each field in objectat(scan): if field != null: field = copyroot(field) scan += object_size(scan) ```

Pros:

  • Compaction is natural (to-space is compact).
  • Allocation is pointer bump (very fast).
  • No fragmentation.

Cons:

  • Requires double memory (unless semi-space is resized; but generational copying avoids full doubling).
  • Copying overhead for live objects.
  • Not well-suited for large heaps without generational techniques.

Generational collection Core idea:

  • Most objects die young (weak generational hypothesis). Partition heap into generations, typically nursery (young) and tenured (old). Do frequent minor collections on nursery (fast copying), occasional major collections on tenured space.
  • Track intergenerational pointers from old to young using write barriers and remembered sets; roots include old->young pointers.

Pros:

  • Good throughput and low pause times for many workloads: most collections are minor and fast.
  • Fast allocation (bump-pointer in nursery).

Cons:

  • Complexity (barriers, remembered sets, promotion policies).
  • Workloads with long-lived short-lived objects or huge volumes of promoted objects can defeat benefits.

Incremental and concurrent collectors Motivation:

  • Long stop-the-world pauses unacceptable; design collectors that run concurrently with mutator to reduce pause times.

Key techniques:

  • Tri-color abstraction to maintain correctness during concurrent marking: objects are white (unprocessed), gray (discoverd but children not yet processed), black (fully processed).
  • Barriers (write barriers and read barriers) to ensure that program modifications do not invalidate collector invariants.
  • Snapshot-at-the-beginning (SATB) and incremental update approaches to implement consistent concurrent marking.

Types:

  • Concurrent mark-sweep (CMS): concurrent marker, stop-the-world remarking, concurrent sweep. Requires avoidance of fragmentation via either mark-compact later or acceptance of fragmentation.
  • Concurrent compacting (Shenandoah/ZGC): concurrent evacuation/compaction using region-based heaps and remembered sets; aims to keep pause times low and independent of heap size.

Pros:

  • Low and predictable pause times.
  • Good for interactive or low-latency servers.

Cons:

  • Overheads from barriers (some percentage of mutator time).
  • Complexity and corner cases (e.g., object pinning, integration with native code).

Parallel collectors

  • Utilize multiple CPU cores to parallelize marking, copying, and sweeping. Most modern server-class GCs do this.
  • E.g., Java's Parallel GC (throughput collector), parallel GCs in GHC, .NET parallel GC during stop-the-world phases.

Real-time collectors

  • Guarantee upper bounds on pause times (hard real-time). Approaches include micro‑scheduling GC work interleaved with mutator and incremental compaction.
  • Examples: Metronome (IBM research), real-time enhancements to Java (Real-Time Specification for Java) use scoped memory or specialized collectors.
  • Typically incur higher overhead for strict guarantees.

Region-based and arena allocation

  • Memory is allocated in arenas or regions: allocation is fast, deallocation occurs by freeing the whole region en masse.
  • Useful for scoped lifetimes (function stack-like lifetimes), efficient where object lifetimes are similar.
  • Regions may be combined with GC for objects escaping region lifetimes.

Conservative garbage collection

  • For languages like C/C++, where pointer metadata is not available, conservative GC scans memory and treats any ...

Ready to see the full tree?

Clone the preview to open the complete learning structure, practice tools, and generated study materials.