Garbage collection (GC) is a form of automatic memory management used in many programming languages and runtime environments. It automatically reclaims memory occupied by objects that are no longer in use by a program, freeing developers from manual memory deallocation and reducing errors such as memory leaks and dangling pointers. Garbage collection algorithms vary in approach, including reference counting, tracing collectors (mark-sweep, mark-compact, copying), and generational collection. GC is a key component of languages such as Java, C#, Go, Python, and JavaScript, and continues to evolve to optimize performance, latency, and memory overhead.

1 Overview and history

1.1 Definition and purpose

Garbage collection (GC) refers to the process of automatically identifying and reclaiming memory that an application no longer needs. Its primary purpose is to eliminate the burden on programmers to manually free memory, thereby preventing two common categories of bugs: memory leaks (failing to release unused memory) and dangling pointers (accessing memory that has already been freed). GC operates as a runtime service, typically running as a background thread or during program execution pauses, to scan the program's memory and determine which objects are still reachable.

1.2 Historical development

1.2.1 Early manual memory management

In early programming languages such as assembly, Fortran, and C, memory management was entirely manual. Programmers explicitly allocated memory with functions like malloc and deallocated it with free. While this gave fine-grained control, it also introduced significant risks. Missteps such as forgetting to free memory, freeing it twice, or accessing freed memory were common sources of crashes and vulnerabilities. As software grew in complexity, the need for automatic memory management became evident.

1.2.2 Lisp and the first GC implementations

The concept of garbage collection was first formally introduced in the late 1950s with the Lisp programming language, developed by John McCarthy at MIT. Lisp's dynamic memory allocation and heavy use of list structures made manual memory management impractical. McCarthy implemented a simple mark-sweep collector in 1960, which served as the foundation for future GC algorithms. Early Lisp systems would pause execution while the collector traversed the memory graph, a technique later named "stop-the-world." This work established GC as a viable alternative to manual management, and subsequent languages such as Smalltalk and Java adopted and refined the idea.

1.3 GC in modern languages

Today, garbage collection is a standard feature of many high-level programming languages and runtimes. Java (1995) popularized GC for mainstream enterprise applications. The Java Virtual Machine (JVM) offered multiple collector options, evolving from simple serial collectors to highly concurrent and low-pause collectors. C# and the .NET platform (2000) integrated a generational GC with tuning for workstation and server scenarios. Python (1991) and JavaScript (1995) use reference counting as their primary mechanism, supplemented with cycle detection. More recently, Go (2009) adopted a concurrent, non-generational GC optimized for low latency. Garbage collection remains an active area of research, with modern collectors balancing throughput, pause time, and memory footprint.

2 Fundamental concepts

2.1 Reachability and roots

An object is considered reachable if it can be accessed directly or indirectly from a set of known starting points called *roots*. Roots typically include global variables, local variables on stack frames, registers, and static fields of classes. Any object that is not reachable from any root is deemed garbage and its memory can be reclaimed. Reachability forms the logical foundation for tracing collectors: they start from the roots and traverse the object graph to mark live objects.

2.2 Mutator and collector

In GC terminology, the *mutator* is the program that modifies the object graph (allocates objects, assigns references) during normal execution. The *collector* is the component that runs concurrently or alternately with the mutator to identify and reclaim garbage. The interaction between mutator and collector is a key design concern: the collector must observe a consistent snapshot of the object graph, often requiring synchronization mechanisms such as write barriers or safepoints.

2.3 Object graphs and garbage detection

Objects form a directed graph where vertices are objects and edges are references from one object to another. Garbage detection reduces to graph reachability analysis: starting from roots, a traversal (depth-first or breadth-first) marks all reachable objects. Any object not visited is garbage. The complexity of this operation depends on the number of live objects and the structure of the graph. Some algorithms (e.g., reference counting) attempt to detect garbage incrementally by tracking reference counts, while tracing collectors perform periodic global traversals.

3 Major garbage collection algorithms

3.1 Reference counting

3.1.1 Algorithm description

Reference counting is a straightforward technique: each object maintains a count of the number of references pointing to it. When a reference is assigned, the count of the target object is incremented; when a reference is removed or overwritten, the count is decremented. If the count drops to zero, the object is immediately deallocated, which may trigger recursive decrements on objects it references.

3.1.2 Advantages and disadvantages

The primary advantage of reference counting is its simplicity and predictable overhead: garbage is reclaimed promptly without requiring global traversals or pauses. However, it has notable disadvantages: it cannot handle cyclic data structures where objects refer to each other in a loop (e.g., a doubly linked list), because their reference counts never reach zero. Additionally, count updates impose overhead on every pointer assignment, and maintaining atomic counts in multithreaded contexts can degrade performance.

3.1.3 Variations (deferred, weighted, cyclic)

Several variations address reference counting's limitations. *Deferred reference counting* delays decrement operations to reduce overhead by batching them. *Weighted reference counting* distributes the count among multiple owners via weights, enabling efficient partial releases. To handle cycles, many runtimes (e.g., CPython) supplement reference counting with a periodic cycle detector that uses a tracing algorithm (often mark-sweep) to reclaim unreachable cycles. This hybrid approach retains the immediate reclamation of non-cyclic garbage while resolving the cycle problem.

3.2 Tracing collectors

3.2.1 Mark-sweep

Mark-sweep is the classic tracing algorithm. It operates in two phases: first, it traverses the object graph from the roots, marking every reachable object. Second, it sweeps through the entire heap, deallocating all unmarked objects and potentially defragmenting memory (if sweeps are followed by compaction, or simply linking freed blocks into a free list). Mark-sweep is simple but suffers from fragmentation and can cause long pauses if the heap is large, as it must scan all memory.

3.2.2 Mark-compact

Mark-compact improves upon mark-sweep by eliminating fragmentation. After the mark phase, the collector relocates all live objects to contiguous memory, leaving a single free block at the end. Compaction may involve updating all references to moved objects, which adds overhead. The benefit is that allocation becomes fast (bump-pointer) and memory utilization remains high. However, the compact phase also increases pause times, especially for large heaps.

3.2.3 Copying collection (stop-and-copy, Cheney's algorithm)

Copying collection divides the heap into two semispaces (typically from-space and to-space). Only one semispace is active at a time. During a collection, all live objects in from-space are copied to to-space, and references are updated. After the copy, from-space is considered empty and the roles of the semispaces are swapped. Cheney's algorithm (1970) performs this copying via a breadth-first traversal using two pointers (scan and free) in to-space, avoiding recursion. Copying collection is fast (proportional to live objects, not total heap), ensures compaction, and has predictable allocation (bump-pointer). However, it wastes half of the heap and may perform poorly if many objects survive long term.

3.3 Generational collection

3.3.1 Young and old generations

Generational collection exploits the empirical observation that most objects die young (the *weak generational hypothesis*). The heap is partitioned into *generations*, typically a *young generation* (sometimes further divided into Eden and survivor spaces) and an *old generation*. New objects are allocated in the young generation. After surviving a few collections, objects are promoted (tenured) to the old generation, which is collected less frequently.

3.3.2 Minor and major collections

Collections of the young generation are called *minor collections*. They are fast because the young generation is small and most objects there are garbage. *Major collections* involve both the young and old generations, or sometimes the old generation alone. They are more expensive and occur less frequently. Hybrid collectors may also perform full heaps collections as a last resort.

3.3.3 Card marking and remembered sets

To enable minor collections without scanning the entire old generation for references to young objects, generational collectors use *remembered sets* (or *card marking*). A card is a fixed-size region of the heap (e.g., 512 bytes). The runtime maintains a byte array (card table) where each entry indicates whether the corresponding card contains a reference that points into the young generation. When the mutator writes a reference from an old object to a young object, a write barrier sets the card as dirty. During a minor collection, only dirty cards are scanned for roots into the young generation, drastically reducing overhead.

4 Implementation considerations

4.1 Parallel and concurrent GC

4.1.1 Parallel stop-the-world

Parallel garbage collection uses multiple threads to perform a stop-the-world collection. All mutator threads are paused, and the collector threads work in parallel to mark and sweep/compact. This reduces wall-clock pause time compared to single-threaded collectors, but the application is still fully suspended. Parallel collectors are common in throughput-oriented scenarios (e.g., batch processing).

4.1.2 Concurrent marking and sweeping

Concurrent collection allows the mutator to run simultaneously with the collector, reducing pause times. The collector may perform most of its work while the application executes, using techniques such as incremental marking or concurrent sweeping. Write barriers ensure that the collector sees a consistent view of the object graph despite mutator changes. Examples include CMS (Concurrent Mark Sweep) in the JVM and the Go runtime's concurrent collector. The trade-off is increased complexity and potential CPU contention.

4.2 Real-time and incremental GC

4.2.1 Time-bound guarantees

Real-time garbage collection is designed for systems with strict timing requirements (e.g., embedded control, audio processing). These collectors provide guarantees on maximum pause time and sometimes on total GC overhead per unit time. They achieve this by breaking the collection into small, bounded increments that are interleaved with mutator execution. The Metronome and Staccato collectors in Java are examples.

4.2.2 Tricolor abstraction

To reason about concurrent and incremental collection, the *tricolor abstraction* is used: objects are colored white (unvisited, presumed garbage), gray (visited but whose children have not been fully scanned), or black (visited and all children scanned). A correct collector must ensure no black-to-white references exist that would cause live white objects to be missed. A write barrier (e.g., Dijkstra's or Steele's barrier) prevents such references from being created during concurrent marking. The tricolor abstraction underpins the design of many modern collectors.

4.3 GC tuning and performance metrics

4.3.1 Throughput, pause time, and footprint

Three primary performance metrics guide GC tuning: *throughput* (the proportion of total CPU time spent on application work, versus GC overhead), *pause time* (the maximum duration of a stop-the-world event), and *footprint* (the memory overhead, including heap size and collector metadata). Different applications prioritize different trade-offs: interactive apps favor low pause times; batch jobs may prioritize high throughput.

4.3.2 Heap sizing and GC triggers

The runtime typically adjusts heap size dynamically based on allocation rates and available memory. GC is triggered when the heap occupancy exceeds a threshold (e.g., the old generation fills up or the young generation survivor spaces overflow). Tuning parameters such as initial heap size, maximum heap size, generation sizes, and GC frequency allow developers to balance performance. Modern collectors (e.g., G1, ZGC) provide more self-tuning capabilities, reducing the need for manual tuning.

5 GC in specific platforms

5.1 Java HotSpot (JVM)

5.1.1 Serial, Parallel, CMS, G1, ZGC, Shenandoah

The Java Virtual Machine (HotSpot) offers a range of garbage collectors. The Serial collector uses a single thread for stop-the-world collections and is suitable for small heaps or single-threaded applications. The Parallel collector (also called throughput collector) uses multiple threads for young and full GCs. CMS (Concurrent Mark Sweep) aimed at low pause times by running most work concurrently, but has been deprecated due to fragmentation and high CPU usage. G1 (Garbage-First) divides the heap into regions and performs incremental, generational collection with predictable pause targets. ZGC (since JDK 11) is a concurrent, region-based collector with sub-millisecond pauses, using colored pointers and load barriers. Shenandoah (since JDK 12) is similar in goal, performing concurrent compaction using Brooks pointers. These collectors reflect the ongoing evolution toward lower latency.

5.2 .NET Common Language Runtime

5.2.1 Workstation vs. server GC

The .NET runtime (CLR) offers two primary GC modes. Workstation GC is optimized for client applications with low concurrency, using a single background thread for collections and aiming for low overhead on user interfaces. Server GC is designed for multiprocessor servers; it creates a separate heap and collector thread per logical processor, improving throughput and scalability for high-concurrency applications. The server mode also parallelizes marking and sweeping phases.

5.2.2 Background collection

Starting with .NET Framework 4.0, the CLR introduced *background collection* (formerly called concurrent GC). A background thread performs a full GC of the old generation while the application continues to run. During background marking, small foreground (ephemeral) collections can still occur, allowing the runtime to manage the young generation concurrently. This reduces pause times for server and workstation modes alike.

5.3 Go runtime

5.3.1 Non-generational concurrent GC

Go's garbage collector is a concurrent, tri-color, mark-sweep collector that is non-generational (the heap is not divided by object age). It was redesigned in Go 1.5 to achieve low pause times (typically under 1 millisecond). The collector runs concurrently with the mutator using a write barrier and a dedicated GC background worker. Because there are no generations, the collector must occasionally scan the entire heap, but it compensates with efficient marking and low overhead.

5.3.2 Write barrier and mutator assists

During concurrent marking, Go uses a *write barrier* implemented as a simple check on pointer writes. When the GC is active, every pointer assignment also records the previous and new values to ensure correctness under the tricolor abstraction. Additionally, if the mutator allocates memory faster than the collector can mark, the mutator may be forced to *assist* by performing some scanning work, preventing the heap from growing unbounded. This mechanism ensures GC progress and bounds pause times.

5.4 Python (CPython)

5.4.1 Reference counting primary

CPython, the reference implementation of Python, uses reference counting as its primary memory management technique. Every Python object has an ob_refcnt field. Assignments, deletions, and function calls adjust counts. When the reference count reaches zero, the object's memory is reclaimed immediately. This gives deterministic deallocation for most objects and avoids global pauses.

5.4.2 Cycle detector (mark-sweep)

Because reference counting alone cannot handle reference cycles (e.g., objects referring to each other), CPython includes a *cyclic garbage collector* based on a mark-sweep algorithm. The collector periodically inspects objects known to be part of containers (lists, dicts, custom objects) and uses a generation-based scheme (three generations) to detect and break cycles. The cycle detector runs during certain allocation thresholds and can be triggered or disabled programmatically.

6 Advanced topics

6.1 Region-based memory management

Region-based memory management, also called *arena allocation*, divides memory into logical regions. Objects are allocated in a region, and the entire region is deallocated at once when the region's lifetime ends (e.g., at function exit or frame completion). This approach avoids per-object GC overhead but requires the programmer to define regions. Some runtimes (e.g., the Cyclone language) combined regions with static analysis, while modern systems like Rust's allocator-api use region-like strategies sparingly.

6.2 Heap partitioning and NUMA awareness

On systems with Non-Uniform Memory Access (NUMA), memory access times depend on which processor socket the memory is attached to. Advanced garbage collectors partition the heap into NUMA-local regions and preferentially allocate objects on the same socket as the thread that created them. Collectors like the JVM's G1 can be configured with NUMA awareness to reduce cross-socket memory traffic and improve performance.

6.3 Deterministic GC and real-time systems

Deterministic garbage collection provides guaranteed upper bounds on pause times and total collection overhead, making it suitable for real-time applications. Approaches include *time-based scheduling* of incremental collections, *stack-based GC* (e.g., the Jikes RVM's SableVM), and hardware-assisted techniques. Though rare in mainstream runtimes, deterministic GC has been implemented in specialized environments like the Fiji VM for Java.

6.4 GC in embedded and memory-constrained environments

Embedded systems with limited memory (e.g., microcontrollers) often avoid traditional GC due to its memory and performance overhead. However, some runtimes (e.g., MicroPython, Espruino for JavaScript) implement simplified GC algorithms such as a compacting semispace collector or a mark-sweep with small heap sizes. Memory constraints force extreme efficiency: collectors may run on every allocation, and heap fragmentation is tightly controlled.

7 Criticism and alternatives

7.1 Performance overhead and unpredictability

Critics of garbage collection point out that automatic memory management introduces CPU and memory overhead. Tracing collectors cause unpredictable pause times, which can break real-time guarantees or degrade user experience in interactive applications. Even concurrent collectors consume CPU cycles and require write barriers that slow down pointer operations. For performance-critical systems like games and high-frequency trading, GC overhead may be unacceptable.

7.2 Manual memory management (RAII)

Resource Acquisition Is Initialization (RAII) is a pattern used in C++ and other languages where memory and other resources are tied to object lifetimes and released automatically via destructors when objects go out of scope. RAII provides deterministic cleanup without a garbage collector. It avoids runtime overhead but requires careful ownership semantics and can be error-prone (e.g., double deletion) if not handled correctly with smart pointers.

7.3 Ownership and borrow checking (Rust)

Rust's ownership system enforces memory safety at compile time through a set of rules: each value has exactly one owner, and references must follow borrowing constraints. The compiler checks these rules, preventing dangling pointers and memory leaks (except for cyclic reference counted types) without a garbage collector. This approach combines the performance of manual management with the safety of GC, but imposes a learning curve on programmers.

7.4 Allocator-based strategies (arena, pool)

Arena allocators (also called region allocators) allow bulk allocation and deallocation of many objects at once. A memory pool pre-allocates fixed-size blocks for objects of the same type. These strategies are used in game engines and low-latency systems to amortize memory management costs. They offer deterministic performance but require the programmer to design allocation policies and manage lifetimes manually.

8 Future directions

8.1 Hardware-assisted GC

Future processors may include hardware support for garbage collection, such as tagged memory, object reference counting circuitry, or specialized instructions for read/write barriers. Intel's MPX (Memory Protection Extensions) and similar technologies are early steps, though not yet widely adopted. Hardware-assisted GC could reduce the overhead of concurrent collecting and enable finer-grained tracing.

8.2 Persistent memory support

Emerging persistent memory technologies (e.g., Intel Optane DC Persistent Memory) blur the line between RAM and storage. Garbage collectors must adapt to manage both volatile and persistent heaps, ensuring transactional consistency and crash recovery. Research explores extending existing collectors (e.g., JVM's ZGC) to handle persistent memory regions while maintaining performance.

8.3 AI-driven heap optimization

Machine learning techniques are being applied to optimize GC behavior. AI models can predict allocation patterns, tune heap sizes, and decide when to trigger collections based on application-specific heuristics. For example, reinforcement learning agents have been used to adjust GC parameters in the JVM to minimize pause times without user intervention. This personalization promises better out-of-the-box performance across diverse workloads.