W3docs

Java GC Algorithms

Compare the major Java garbage collectors — Serial, Parallel, G1, ZGC, and Shenandoah — and their trade-offs.

Java GC Algorithms

The JVM frees you from manually deallocating memory: a garbage collector (GC) runs in the background, finds objects your program can no longer reach, and reclaims their space. But "the garbage collector" is not one thing. The HotSpot JVM ships several collectors, each making a different bargain between throughput (how much CPU goes to your application rather than to GC), latency (how long the application pauses while GC works), and footprint (how much memory the collector itself overhead costs).

Choosing well matters. A batch job that crunches numbers overnight wants maximum throughput and does not care about pauses; a trading system or a web server wants the shortest possible pauses even if total throughput drops a little. This chapter compares the production collectors and shows what they all have in common: an object lives exactly as long as it is reachable.

How collectors decide what to keep

Every HotSpot collector answers the same question — which objects are still in use — the same way: it traces reachability from a set of GC roots (local variables on the stack, static fields, active threads). Any object reachable from a root is live; everything else is garbage. Scope and age are irrelevant; only reachability counts.

public class Reachability {
    public static void main(String[] args) {
        String a = new String("kept");      // reachable via local variable 'a'
        String b = new String("dropped");   // reachable via 'b'...
        b = null;                            // ...until now: "dropped" is unreachable
        System.out.println(a);               // 'a' is still a GC root reference
    }
}

The moment b = null runs, the "dropped" object has no path from any root and becomes eligible for collection. The collector may reclaim it immediately, much later, or — if the program exits first — never. You never call free; you simply stop referencing.

Generational heap layout

Most Java objects die young. Collectors exploit this with a generational heap: new objects land in the young generation (Eden plus two survivor spaces), and objects that survive several collections are promoted to the old generation. Collecting the small, garbage-heavy young generation frequently is cheap; the large old generation is collected far less often.

RegionWhat lives hereHow often collected
EdenFreshly allocated objectsEvery minor GC
Survivor (S0/S1)Objects that survived a minor GCEvery minor GC
Old (tenured)Long-lived, promoted objectsMajor / full GC
MetaspaceClass metadata (off-heap)On class unloading

A minor GC cleans the young generation and is fast; a major or full GC touches the old generation and is the source of the long pauses people fear.

The collectors compared

HotSpot lets you pick a collector with a single flag, and each one is tuned for a different goal. You rarely change the algorithm in code — you set it on the command line.

java -XX:+UseSerialGC     MyApp   # single-threaded, tiny heaps
java -XX:+UseParallelGC   MyApp   # throughput-oriented, multi-threaded
java -XX:+UseG1GC         MyApp   # balanced, the default since Java 9
java -XX:+UseZGC          MyApp   # sub-millisecond pauses, huge heaps
java -XX:+UseShenandoahGC MyApp   # low pause, concurrent compaction

The table below is the mental model to carry around:

CollectorStrengthPause behaviorTypical use
SerialSimplest, low footprintStop-the-world, single threadSmall heaps, containers, CLIs
ParallelHighest throughputStop-the-world, many threadsBatch / data processing
G1Balanced, predictableMostly concurrent, target pauseGeneral-purpose default
ZGCVery low latencySub-millisecond, concurrentMulti-GB to TB heaps
ShenandoahVery low latencyPauses independent of heap sizeResponsive services

G1 ("Garbage-First") is the default from Java 9 onward. It divides the heap into equal-size regions and collects the regions with the most garbage first, steering toward a pause-time target you set with -XX:MaxGCPauseMillis=200.

Concurrent vs stop-the-world

The crucial axis is when the application threads have to stop. Stop-the-world (STW) collectors (Serial, Parallel) pause all application threads while they work — simple and high-throughput, but the pause grows with the heap. Concurrent collectors (ZGC, Shenandoah, and most of G1) do the bulk of their work while your threads keep running, so pauses stay short even as heaps reach gigabytes or terabytes.

# See exactly what the collector is doing and how long it pauses
java -Xlog:gc -XX:+UseG1GC MyApp
# Sample output line:
# [0.412s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 24M->5M(64M) 1.832ms

That log line is worth decoding: 24M->5M(64M) means used heap dropped from 24 MB to 5 MB out of a 64 MB total, and the application paused for 1.832ms. Reading -Xlog:gc output is the single most useful GC-tuning skill — measure before you change a flag.

Observing collection from code

You cannot directly invoke a specific algorithm from Java, but you can observe collection happening. A WeakReference lets you hold a pointer that does not keep its target alive, so you can ask "has this object been collected yet?" The Runtime class reports heap usage, and System.gc() is a hint — never a command — to run a collection now.

import java.lang.ref.WeakReference;

WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]);
System.out.println("Before GC: " + (ref.get() != null)); // true
System.gc();
System.out.println("After GC:  " + (ref.get() != null)); // usually false

The runnable example below puts these pieces together: it allocates a wave of garbage, keeps one survivor reachable, watches an unreachable object through a weak reference, and measures the heap before and after a collection.

java— editable, runs on the server

What to take from the run:

  • Step 2 shows used heap climbing after 300,000 short-lived allocations — that surge is exactly the young-generation garbage every generational collector is built to sweep cheaply.
  • Step 3 prints true: a WeakReference does not keep its target alive, yet the object is still reachable enough to read until a collection actually runs.
  • Step 4 prints true, confirming the unreachable object was reclaimed once System.gc() triggered a collection — proof that loss of reachability, not going out of scope, is what frees memory.
  • Step 5 prints true: the survivor, still referenced by a live local variable (a GC root), crosses the collection untouched.
  • Step 6 shows used heap falling back near the baseline, demonstrating that the collector returns reclaimed space for reuse rather than the program leaking it.

Practice

Practice

What single property determines whether the garbage collector will keep an object, regardless of which algorithm is in use?