Garbage Collectors: Serial, Parallel, G1, ZGC, Shenandoah
How each collector works, the throughput-versus-latency trade-off that separates them, what G1 regions and pause targets really do, and how ZGC achieves sub-millisecond pauses on huge heaps.
On this page
Every collector makes the same trade: shorter pauses cost throughput, because concurrent work requires coordination with running application threads. Understanding that one axis explains the entire lineup.
Key Takeaways
- All tracing collectors do the same job: find what is reachable, reclaim the rest. They differ in when they stop your threads.
- Parallel maximises throughput with long pauses. G1 balances. ZGC and Shenandoah minimise pause time at a throughput cost.
- G1 divides the heap into regions and collects the ones with most garbage first — "garbage first".
- ZGC uses coloured pointers and load barriers to relocate objects while threads run; pauses stay under a millisecond regardless of heap size.
MaxGCPauseMillisis a target, and setting it too low actively harms both throughput and latency.
The common foundation
Every JVM collector is a tracing collector. It starts from GC roots — thread stacks, static fields, JNI references — marks everything reachable, and treats the rest as garbage. There is no reference counting, which is why circular references are collected without special handling.
Three mechanical strategies get combined in different ways:
Mark-sweep marks live objects and frees the rest in place. Fast, but leaves the heap fragmented.
Mark-compact additionally slides survivors together, eliminating fragmentation and making allocation a pointer bump. Slower, because everything moves and every reference must be updated.
Copying moves live objects into a fresh space and discards the old one wholesale. Cost is proportional to the live set, not the heap size — which is exactly why it suits the young generation, where almost everything is dead.
The lineup
| Collector | Flag | Pause | Throughput | Best for |
|---|---|---|---|---|
| Serial | -XX:+UseSerialGC | Long | Good on 1 core | Small heaps, single-CPU containers |
| Parallel | -XX:+UseParallelGC | Long (100ms–seconds) | Highest | Batch jobs, ETL |
| G1 | -XX:+UseG1GC (default) | Moderate (~50–200ms) | Good | Most server applications |
| ZGC | -XX:+UseZGC | Under 1ms | ~10–15% lower | Latency-critical, huge heaps |
| Shenandoah | -XX:+UseShenandoahGC | Under 10ms | ~10% lower | Latency-critical, OpenJDK |
Serial uses one thread and stops the world for everything. That sounds obsolete and is not: in a container limited to one CPU, parallel collectors have no other core to run on, and their coordination overhead makes them slower than Serial. For a small sidecar or a function-style workload, Serial plus a small heap is often the fastest configuration.
Parallel is Serial with multiple GC threads. It still stops the world completely, but it finishes faster and does the least total work per byte reclaimed, which is why it still holds the throughput crown. For an overnight batch job where nobody is waiting on a response, it is the correct choice.
G1
G1 divides the heap into 1–32MB regions, each dynamically labelled eden, survivor, old or humongous. It is generational and it compacts, but it never collects the whole old generation at once.
The cycle is: young collections until old-generation occupancy crosses
InitiatingHeapOccupancyPercent (45% by default), then a concurrent mark to find out which old
regions are mostly garbage, then a series of mixed collections that each clean the young
generation plus a few of the worst old regions.
MaxGCPauseMillis (200ms by default) drives everything: G1 measures how long collecting a region
takes and picks how many regions fit in the budget. Setting it to 10ms does not produce 10ms pauses —
it produces a tiny young generation, extremely frequent collections, high promotion rates and worse
latency overall. This is the single most common G1 misconfiguration.
ZGC and Shenandoah
Both do essentially all of their work concurrently, including relocation — moving objects while application threads read and write them. That is the hard part, and each solves it differently.
ZGC stores metadata in unused bits of 64-bit pointers (coloured pointers) and installs a load barrier: every reference read checks the colour, and if the object has been relocated the barrier fixes the pointer on the spot. Pause times are bounded by the root-scanning work, which does not grow with heap size — so ZGC holds sub-millisecond pauses on a 16TB heap as easily as on a 16GB one.
Shenandoah uses a Brooks forwarding pointer — an extra word in each object header pointing at its current location — with a read barrier that follows it. Similar outcome, different mechanism.
The cost of both is roughly 10–15% throughput, from executing barrier code on essentially every
reference access, plus higher memory overhead. Generational ZGC (-XX:+ZGenerational, Java 21, the
default from 23) recovers much of that by applying the generational hypothesis, and is the version to
reach for.
Choosing
# 1. Heap under ~4GB, one or two CPUs (a small container)
-XX:+UseSerialGC
# 2. Batch job, throughput is everything, pauses irrelevant
-XX:+UseParallelGC
# 3. Anything else — the default, and correct for most services
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# 4. p99 latency matters more than throughput, or heap > 32GB
-XX:+UseZGC -XX:+ZGenerationalTwo practical notes. Crossing 32GB of heap disables compressed oops, so references grow from 4 to 8 bytes and effective capacity drops — a 32GB heap can hold less live data than a 31GB one. And the collector is rarely the first thing to tune: excessive allocation is usually the real problem, and no collector fixes an application producing a gigabyte of garbage per second.
Measuring the difference
-Xlog:gc*:file=gc.log:time,uptime,level,tags -Xmx4g -XX:+UseG1GCRun your real workload under each candidate and compare three numbers: throughput (percentage of wall-clock time not spent in GC — above 95% is healthy), p99 pause time, and allocation rate. Synthetic benchmarks mislead badly here, because collector behaviour depends entirely on the shape of your object graph and lifetimes.
Reading those logs is covered in GC logs and tuning.
What gets asked
"What garbage collectors do you know and when would you use each?" — answer along the throughput/latency
axis rather than listing names. Then: how G1 differs from Parallel (regions, concurrent marking,
partial old collections); what MaxGCPauseMillis does and why setting it too low backfires; and how
ZGC achieves sub-millisecond pauses. Mentioning the 32GB compressed-oops cliff unprompted is a strong
signal.
Frequently Asked Questions
Which garbage collector should I use?
What is a stop-the-world pause?
Does MaxGCPauseMillis guarantee a maximum pause?
Related tutorials
- JVM Memory Areas: Heap, Stack, Metaspace, DirectEvery region the JVM allocates, which are per-thread and which are shared, why the heap is generational, where Metaspace lives since Java 8, and why total process memory always exceeds Xmx.
- Reading GC Logs and Tuning Without GuessingEnabling unified GC logging, reading a G1 log line by line, calculating allocation and promotion rates, identifying every Full GC cause, and the tuning changes that are usually wrong.
- The Seven Classic Java Memory LeaksThe seven leak patterns that recur in every codebase, why a garbage-collected language leaks at all, how each one is diagnosed from a heap dump, and the code change that fixes each.
- Every OutOfMemoryError and What It MeansEach OutOfMemoryError message, what it actually indicates, the most likely cause, and the first three things to check — plus why OOMKilled by the kernel is a different failure entirely.