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

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
- Latency: how long the application is stopped during a collection. Matters for interactive services with tight response-time objectives.
- Throughput: the share of CPU time spent running application code rather than collecting. Matters for batch jobs, where only total run time counts.
- Footprint: memory and CPU the collector needs for itself, such as extra heap headroom, remembered sets and concurrent threads. Matters for small containers and dense deployments.
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
| Collector | Flag | Design in one line | Typical fit |
|---|---|---|---|
| Serial | -XX:+UseSerialGC | Single-threaded, stop-the-world, generational | Small heaps, one CPU, short-lived tools |
| Parallel | -XX:+UseParallelGC | Multi-threaded, stop-the-world, generational | Batch and throughput workloads that tolerate pauses |
| G1 | -XX:+UseG1GC | Region-based, mostly concurrent marking, pause-time target | General purpose; the default |
| ZGC | -XX:+UseZGC | Concurrent relocation; pauses do not grow with heap size | Latency-sensitive services, large heaps |
| Shenandoah | -XX:+UseShenandoahGC | Concurrent compaction | Latency-sensitive services, where the build includes it |
| Epsilon | -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC | Allocates, never collects | Benchmarks and very short-lived jobs only |
A short history worth knowing
- G1 became the default collector in JDK 9 (JEP 248), replacing Parallel.
- G1's full collections became parallel in JDK 10 (JEP 307).
- ZGC arrived as experimental in JDK 11 (JEP 333) and became a product feature in JDK 15 (JEP 377). Generational ZGC was added in JDK 21 (JEP 439), became ZGC's default mode in JDK 23 (JEP 474), and the non-generational mode was removed in JDK 24 (JEP 490).
- Shenandoah was integrated as experimental in JDK 12 (JEP 189) and made a product feature in JDK 15 (JEP 379). Not every JDK build includes it, so check with your vendor.
- Epsilon was added in JDK 11 (JEP 318).
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
- Run the same load for long enough to include several old-generation or full cycles.
- Keep the heap size identical when comparing collectors, and set
-Xmsequal to-Xmxif you want to remove resizing from the comparison. - Compare application metrics (p99 latency, throughput, CPU) as well as GC logs. A collector with shorter pauses but more CPU can make a CPU-limited service slower.
- Record the GC log for both runs with the same configuration:
-Xlog:gc*:file=gc.log:time,uptime,level,tags. Reading GC logs explains what to compare.
Signs you picked the wrong collector
- Long, regular pauses on a latency-sensitive service running Parallel or Serial: the collector is doing what it was designed to do. Move to G1, or to ZGC if G1's pauses are still too long.
- High GC CPU with a concurrent collector on a small container: concurrent threads compete with the application for one or two cores. G1 or even Serial may give better results there.
- Frequent allocation stalls with ZGC, or degenerated and full cycles with Shenandoah: the heap is too small for the allocation rate while collection runs concurrently. Add headroom before blaming the collector.
- Unexpected Serial: the machine or container did not qualify as server-class. Choose the collector explicitly.
Sources
- HotSpot Virtual Machine Garbage Collection Tuning Guide and its ergonomics chapter
- The java command (collector flags, MaxGCPauseMillis)
- JEPs 248, 307, 333, 377, 439, 474, 490, 189, 379, 318
Garbage collectionTuning G1
Heap size first, a realistic pause target, then IHOP and region size, and why -Xmn works against G1.
Garbage collectionReading GC logs with -Xlog
One line of configuration, the legacy flag mapping, and how to read pause lines for leaks and undersized heaps.
ProductionThe JVM in containers
How limits are detected, what MaxRAMPercentage really sets, and a budget that keeps the OOM killer away.