Garbage collection

Choosing a HotSpot garbage collector: Serial, Parallel, G1, ZGC or Shenandoah

Garbage collection · Published 2026-10-22

Three blue recycling bins outdoors, filled with glass bottles, plastic and cardboard
Different collectors sort and reclaim memory in different ways. Photo: Alabama Extension, Flickr, CC0.

HotSpot ships several garbage collectors because no single design is best at everything. Each one trades between three goals: short pauses, high throughput and small memory footprint. Choosing well starts with knowing which of the three your service actually needs.

The three goals

The HotSpot GC Tuning Guide frames collector selection in these terms and generally advises starting with the default and letting ergonomics size the heap before tuning anything.

The collectors

CollectorFlagDesign in one lineTypical fit
Serial-XX:+UseSerialGCSingle-threaded, stop-the-world, generationalSmall heaps, one CPU, short-lived tools
Parallel-XX:+UseParallelGCMulti-threaded, stop-the-world, generationalBatch and throughput workloads that tolerate pauses
G1-XX:+UseG1GCRegion-based, mostly concurrent marking, pause-time targetGeneral purpose; the default
ZGC-XX:+UseZGCConcurrent relocation; pauses do not grow with heap sizeLatency-sensitive services, large heaps
Shenandoah-XX:+UseShenandoahGCConcurrent compactionLatency-sensitive services, where the build includes it
Epsilon-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGCAllocates, never collectsBenchmarks and very short-lived jobs only

A short history worth knowing

The consequence for anyone running JDK 21: -XX:+UseZGC alone selects the non-generational mode there, and generational ZGC needs -XX:+UseZGC -XX:+ZGenerational. From JDK 23 the generational mode is the default, and from JDK 24 it is the only one.

What the JVM picks if you say nothing

The default is not always G1. The ergonomics chapter of the tuning guide states that G1 is selected on server-class machines and Serial otherwise, and that the VM considers a machine server-class when it detects two or more processors and at least 1792 MB of physical memory.

Inside a container, "detected" means the container's limits (see the JVM in containers). A pod limited to one CPU or to less than 1792 MB gets the Serial collector unless you choose otherwise. That is often a reasonable choice for a tiny service, but it surprises people who expected G1's behaviour. Check what you actually got:

$ java -XX:+PrintCommandLineFlags -version
# or on a running JVM
$ jcmd <pid> VM.flags | tr ' ' '\n' | grep -E 'Use.*GC$'

The GC log also names the collector in its first lines when started with -Xlog:gc or -Xlog:gc*.

A practical way to decide

Start from the default

For most services, G1 with an explicit heap size is the right starting point. It aims at a pause-time target (200 ms by default, per the java reference) and balances throughput and latency reasonably without tuning. Tuning G1 covers the few options worth touching.

Move to ZGC or Shenandoah when pauses are the problem

If GC pauses show up in your tail latency and G1 tuning does not bring them within your objective, try a concurrent compacting collector. The java reference describes ZGC as a low-latency collector providing maximum pause times of a few milliseconds, at some throughput cost. Expect higher CPU use for the same work and give it heap headroom: concurrent collectors need free memory to keep allocating while they work.

Move to Parallel when only total time matters

For a nightly batch, a data pipeline stage or a build tool, pauses are irrelevant and Parallel can finish the same work with less GC overhead. Measure total run time, not pause times.

Use Serial for the very small

Single-CPU containers, command-line tools and small sidecars: Serial has the smallest overhead and no concurrent threads competing for a single core.

Measure the switch properly

Signs you picked the wrong collector

Sources