How Garbage Collection Works

Automatic memory management. See how runtimes find and reclaim unreachable objects.

Heap Memory
1
2
3
4
5
6
7
Live (Unmarked)
Marked
Garbage
1 / 6

Object Allocation

Creating objects

What Happens

When you create an object (new Object()), memory is allocated on the heap.

Why

The heap is where dynamically-sized data lives. Stack is for fixed-size, short-lived data.

Technical Detail

Object header + fields. Pointer to vtable/class info.

Example let user = { name: "Ted" }; → Heap allocation

Key Takeaways

Reachability

Object is live if reachable from roots. Otherwise it's garbage.

Generational

Most objects die young. Collect young gen frequently.

Pause Times

GC pauses impact latency. Use concurrent collectors for low-latency apps.

The Engineering of Garbage Collection: Taming the Heap

In languages like C or C++, engineers must manually request memory from the operating system using malloc() and, crucially, manually return it using free(). Forgetting to free memory causes memory leaks; freeing memory twice causes catastrophic crashes. Modern languages like Java, Go, C#, and JavaScript employ a high-stakes, invisible background worker called the Garbage Collector (GC) to strictly manage this memory, trading a small amount of CPU overhead for complete spatial safety.


Part 1: The Stack vs. The Heap

To understand Garbage Collection, you must understand where data lives. When a function executes, its local, fixed-size variables (like integers and booleans) are pushed onto the Call Stack. When the function returns, the Stack pointer simply moves down, instantly "deleting" those variables. The Stack manages itself automatically at near-zero CPU cost.

However, dynamically-sized data (like an Array that grows to 10,000 items, or an Object instantiated with new User()) cannot live on the fixed-size Stack. It must be allocated on the Heap.

The Heap is a massive, unstructured ocean of RAM. The Stack only holds pointers (memory addresses) that point across the void into the Heap. When a function finishes and its Stack pointers are destroyed, the giant Object on the Heap is suddenly utterly isolated. It takes up RAM, but the application has mathematically lost the ability to ever interact with it again. This is Garbage.

Part 2: Reachability (The Mark Phase)

How does the GC know what is Garbage and what isn't? It uses a Graph Traversal algorithm called Reachability Analysis.

The GC defines strict starting points called GC Roots. These are typically global variables, executing CPU registers, and the currently active local variables on the Call Stack.

During the Mark Phase, the GC pauses the entire application, starts at the GC Roots, and meticulously follows every single pointer into the Heap. When it finds an Object, it forcefully flips a hidden "Mark Bit" in the Object's memory header to 1 (Alive). If that Object points to other Objects, it follows those too, recursively scanning the entire memory graph.

When the traversal finishes, any Object in the Heap that still has its Mark Bit set to 0 is mathematically unreachable. It is declared dead.

Part 3: The Sweep and Compact Phases

Once the dead objects are identified, the GC enters the Sweep Phase. It scans linearly across the entire Heap memory space. Whenever it encounters a dead Object (Mark Bit 0), it instructs the Operating System that this specific chunk of RAM is now free to be overwritten by future allocations.

However, sweeping leaves "holes" in the memory (Fragmentation). If you allocate and free thousands of small objects, your 10GB Heap might be 90% empty, but completely shattered into tiny 8-byte gaps. If you try to allocate a large 10MB Array, it will fail because there is no contiguous empty space.

To fix this, advanced GCs initiate a Compact Phase. The GC literally halts the program, picks up the live Objects, slides them all the way to the left side of the memory space to squash out the gaps, and meticulously rewrites every single pointer on the Call Stack to point to the new physical RAM addresses.

Part 4: The Generational Hypothesis (V8 / JVM)

Traversing the entire object graph of a 64GB Heap takes seconds. Pausing a server for 3 seconds is unacceptable. Enter the Generational Hypothesis, the cornerstone of V8 (Node.js) and the JVM (Java).

IBM researchers discovered a universal law of software: "Most objects die young." Temporary string concatenations and loop scoped variables are garbage almost instantly. Objects that survive a long time (like Database Connection Pools or Caches) tend to survive forever.

Modern GCs physically split the Heap into two separate zones:

  • The Young Generation (Nursery): Very small (e.g., 32MB). All new objects are allocated here. When it fills up, a "Minor GC" occurs. It is blazingly fast because it only scans the tiny young space. The 95% of objects that died immediately are flushed.
  • The Old Generation (Tenured): Objects that survive multiple Minor GCs are forcibly promoted (copied) into the massive Old Generation. The GC rarely scans this area. Only when the Old Generation is completely full does the runtime trigger a "Major GC" (Full Mark-Sweep-Compact), which causes the dreaded latency spikes.

Part 5: Stop-The-World vs. Concurrent GC

A Stop-The-World (STW) pause is when the GC instructs the Operating System to violently suspend all executing application threads. Why? Because if the Application continues to run while the GC is analyzing the Reachability graph, the Application might aggressively mutate pointers, hiding a dead object from the scanner, or worse, causing the GC to delete a live object.

Modern engineering is obsessed with eliminating STW pauses.

Languages like Go prioritize ultra-low latency. The Go Garbage Collector is a Concurrent Mark-and-Sweep collector. It runs at the exact same time as your application code.

How does it prevent the application from moving pointers while the scanner is running? It injects highly optimized Assembly instructions called Write Barriers. During the GC phase, if your code attempts to rewrite a pointer, the CPU intercepts the instruction, briefly alerts the GC ("Hey, I'm moving this object over here!"), and then proceeds. This consumes roughly 25% of the CPU's total power during a GC cycle, but it ensures STW pauses rarely exceed 1 millisecond, even on gargantuan 100GB Heaps.

Conclusion: The Eternal Tradeoff

Garbage Collection is not a solved problem; it is a spectrum of engineering tradeoffs.

You can have maximum Throughput (doing the most total work per second) by letting garbage pile up and doing massive, highly-efficient STW Pauses (Java's Parallel GC). Or, you can have ultra-low Latency (never pausing for more than 1ms) by sacrificing 30% of your CPU power to run Concurrent Write Barriers (Go, Java ZGC). You must choose the engine that matches your physical constraints.

Glossary & Concepts

GC Roots

Starting points for reachability analysis: stack variables, global/static refs, CPU registers.

Mark & Sweep

Classic GC algorithm: mark all reachable objects, then sweep (free) unmarked objects.

Generational Hypothesis

Most objects die young. New objects go to "young gen" and are collected frequently.

Stop-the-World (STW)

GC pauses all application threads to safely collect garbage. Causes latency spikes.

Concurrent GC

Modern collectors (G1, ZGC, Shenandoah) run mostly concurrent with app threads for low pause times.

Write Barrier

Code inserted by the runtime to track pointer mutations during concurrent marking.