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.
| Region | What lives here | How often collected |
|---|---|---|
| Eden | Freshly allocated objects | Every minor GC |
| Survivor (S0/S1) | Objects that survived a minor GC | Every minor GC |
| Old (tenured) | Long-lived, promoted objects | Major / full GC |
| Metaspace | Class 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 compactionThe table below is the mental model to carry around:
| Collector | Strength | Pause behavior | Typical use |
|---|---|---|---|
| Serial | Simplest, low footprint | Stop-the-world, single thread | Small heaps, containers, CLIs |
| Parallel | Highest throughput | Stop-the-world, many threads | Batch / data processing |
| G1 | Balanced, predictable | Mostly concurrent, target pause | General-purpose default |
| ZGC | Very low latency | Sub-millisecond, concurrent | Multi-GB to TB heaps |
| Shenandoah | Very low latency | Pauses independent of heap size | Responsive 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.832msThat 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 falseThe 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.
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: aWeakReferencedoes 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 onceSystem.gc()triggered a collection — proof that loss of reachability, not going out of scope, is what frees memory. - Step 5 prints
true: thesurvivor, 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
What single property determines whether the garbage collector will keep an object, regardless of which algorithm is in use?