Thursday, 27 November 2025

Java Garbage Collection (GC): How Modern JVM GC Works, Evolves, and Scales

Garbage Collection is the JVM’s silent guardian. It quietly reclaims memory from objects your application no longer needs—no manual freeing, no memory leaks (well, mostly), no pointer nightmares.

But as applications scale and heap sizes grow into gigabytes or even terabytes, those tiny moments when GC stops your application (known as Stop-The-World pauses) can become the single biggest threat to performance.

To understand why GC pauses happen—and how modern collectors like G1, ZGC, and Shenandoah nearly eliminate them—we need to start with the basics: how Java organises memory.

Java Heap: Where Objects Live and Die
The design of the Java Heap is based on one powerful, observed truth: the "Weak Generational Hypothesis"—that is, most objects die very young. This insight led to Generational Garbage Collection, where the heap is strategically partitioned based on an object's expected lifespan.


GC Roots
This is the starting line for the GC process. An object is only considered "live" if the GC can trace a path to it from one of these roots. They are the application's solid reference points, the objects that must absolutely not be collected:
    Local variables on your thread stacks.
    Static fields of loaded classes.
    Active threads and native JNI references.

Eden:
Every new object you create with new is born here. This is the most volatile area, constantly being collected by the Minor GC. It acts like a nursery where the majority of objects (≈90% or more) are created and die almost instantly, never leaving this space.

Survivor Spaces (S0 / S1):
Objects that managed to survive their first encounter with the Minor GC in Eden are moved here. They ping-pong back and forth between the two small spaces (S0 and S1). Each time an object survives this trip, its "age" counter ticks up, proving its longevity.

Old Generation: 
Objects that successfully pass a predefined age threshold (usually around 15 minor collections) are considered long-lived and are promoted to the Old Generation. This area contains the stable, long-term residents, and consequently, it is collected much less often by a Major GC or Full GC.

Metaspace: 
This area is technically outside the Heap in native system memory. Since Java 8 (it replaced the old PermGen), Metaspace holds the metadata about the classes your application loads—the structure, names, and methods. It's the blueprint archive for your application's code. 

GC Mechanisms: How Garbage is Found and Removed
How does the JVM actually clean up? There are three primary mechanisms that all GCs use in some combination.
Mark & Sweep:
Mark Phase: The GC walks the object graph starting from the GC Roots and marks everything reachable (live).
Sweep Phase: The GC scans the heap and reclaims memory from unmarked (garbage) objects.
The catch? This leaves the heap with Swiss-cheese-like holes, known as fragmentation. This fragmentation can lead to a dreaded Full GC when the JVM can't find a contiguous space large enough for a new object, even if there is technically enough free memory overall.

Mark–Sweep–Compact: Solving the Fragmentation Problem
To fix fragmentation, a third step is added:
    Compact Phase: All live objects are shuffled to one side of the heap, leaving the free space as one large, clean block. This is great for allocation, but compaction takes time, adding significantly to the STW pause.

The Copying Algorithm :
In the Young Generation, the JVM uses a far faster trick: copying. Instead of marking, sweeping, and compacting, it simply copies live objects from the active spaces (Eden + S0) into the empty space (S1). It then wipes the old spaces clean. Copying is naturally compacting and lightning-fast—this is why Minor GCs are usually so quick.

Tri-Color Marking:
For modern GCs (G1, ZGC, Shenandoah) to work concurrently—meaning the application runs while the GC cleans—they use Tri-Color Marking. This helps the GC understand the current state of objects even as application threads (Mutators) are busy changing references.
    White: Unvisited (suspected garbage).
    Gray: Visited, but its object references have not yet been scanned.
    Black: Visited, and all of its references have been scanned (known-live).

To prevent the application from accidentally hiding a live object (the "tri-color invariant" violation), these GCs use write barriers or load barriers—tiny, quick bits of code inserted by the JVM compiler to manage references whenever the application touches memory.

GC Evolution & Timelines:

Java VersionCollectorNotes
Java 1.3Serial GCFirst simple GC
Java 1.4Parallel GCMultithreaded, throughput-focused
Java 5CMSFirst low-pause GC
Java 7G1 (experimental)Region-based innovation
Java 9G1 defaultCMS deprecated
Java 11ZGC (experimental)Sub-millisecond pauses
Java 15ZGC GAProduction-ready
Java 12–15ShenandoahUltra-low latency
Java 14CMS removedEnd of an era


Serial GC:
Think of Serial GC as a single janitor who locks the doors before cleaning.
    The Vibe: Simple and sequential. It uses a single thread for all collection work.
    The Cost: This is the definition of a Stop-The-World (STW) pause. Every single application thread must halt for both Young and Old generation collections.
    Best For: Tiny stuff. We're talking small clients, embedded systems, or containers with heaps well under 100MB. If you have plenty of CPU cores, don't use this.
    Enable: -XX:+UseSerialGC

Parallel GC:
Parallel GC is the natural evolution of Serial: "If one thread is slow, use ten!"
    The Goal: It’s nicknamed the Throughput Collector because its mission is to maximize the total amount of work your application gets done. It does this by using multiple GC threads to speed up the collection phase.
    The Tradeoff: It still pauses the world (it’s an STW collector), but the pauses are much shorter than Serial. However, on multi-gigabyte heaps, these pauses can still be noticeable—sometimes hitting the half-second or even one-second mark.
    Mechanism: It uses multi-threaded Mark–Sweep–Compact for both Young and Old collections.
    Enable: -XX:+UseParallelGC

CMS (Concurrent Mark Sweep):
CMS was Java's first serious attempt at achieving low latency. It was a game-changer but came with baggage.
    The Breakthrough: It figured out how to do most of the marking concurrently—meaning the GC was tracking objects while your application threads were still running. This dramatically minimized the longest STW pauses.
    The Flaw: CMS was a non-compacting collector. Over time, the heap became terribly fragmented (Swiss cheese holes!). Eventually, the JVM would fail to find a large enough contiguous block for a new object, leading to a catastrophic, hours-long STW Full GC just to compact everything.
    Status: Due to its complexity and fragmentation issues, CMS is considered legacy—it was deprecated in Java 9 and removed entirely in Java 14.
    Enable: -XX:+UseConcMarkSweepGC
    
G1 GC (Garbage-First):
G1 is the modern standard, a massive leap forward that shifted the focus from the whole heap to manageable regions.
    Core Idea: Instead of treating the heap as three fixed blocks (Eden/Survivor/Old), G1 carves it up into ≈2048 fixed-size regions. These regions dynamically switch roles (Young, Old, Humongous) as needed.
    Pause Prediction: G1 tracks which regions have the most garbage (the best "return on investment"). It follows the Garbage-First principle, prioritizing those regions to meet your specified pause time goal (e.g., "I promise to pause no longer than 200ms").
    Collection: It uses Evacuation (copying) to move live objects out of the selected regions. This means it compacts memory as it cleans, eliminating the fragmentation nightmare that plagued CMS. G1 is the default collector since Java 9 for a reason: it's a great all-around performer.
    Enable: -XX:+UseG1GC

ZGC (Ultra-Low Latency):
ZGC is the future. Its design goal was radical: pause times must be independent of the heap size. You can run a TB-sized heap, and your application will pause for the same fraction of a millisecond as a 1GB heap.
    Concurrent Everything: It does marking, relocation, and reference processing all concurrently with the application.
    The Magic: It achieves this via Colored Pointers and Load Barriers. The GC can literally move an object while your application is using it. When your code tries to access the object, the Load Barrier briefly intercepts the call, corrects the old pointer to the object's new location, and lets the application continue. The pause for this fix-up is incredibly brief.
    Pause Time: Guaranteed ≈1−3ms pauses. This is the choice for extreme low-latency and massive memory systems.
    Enable: -XX:+UseZGC

Shenandoah:
Developed by Red Hat (now part of OpenJDK), Shenandoah shares ZGC's goal of achieving ultra-low pause times independent of heap size.
    Similarities: It is also region-based and uses a concurrent approach.
    Distinction: Shenandoah's key innovation is its highly optimized concurrent compaction. It can perform memory consolidation while your application is fully running, ensuring the heap stays compact and healthy without any long STW events.
    Best For: Scenarios similar to ZGC—very large heaps and demanding latency requirements.
    Enable: -XX:+UseShenandoahGC