Back to Blog
java-21garbage-collectionjvmperformanceg1gczgc

Java 21 GC Tuning: G1GC vs ZGC vs Shenandoah - Which One Should You Use? (2026)

Choosing the wrong garbage collector costs you latency, throughput, and money. This guide compares G1GC, ZGC, and Shenandoah in Java 21 with benchmarks and configuration examples.

J

JOptimize Team

May 20, 2026· 9 min read

Garbage collection is the invisible tax on every Java application. Pick the wrong collector for your workload and you pay in latency spikes, throughput loss, or unpredictable pauses. Java 21 ships with three production-ready GCs - G1GC, ZGC, and Shenandoah - and each has a different trade-off profile.


The Three Collectors at a Glance

CollectorDefault sincePause modelBest for
G1GCJava 9Low pause (10-200ms)Most workloads
ZGCJava 15 (prod)Sub-millisecondLarge heaps, latency-sensitive
ShenandoahJava 12 (prod)Sub-millisecondLow-latency, moderate heap

G1GC: The Safe Default

G1GC (Garbage-First) is the default in Java 9+ and the right choice for most Spring Boot applications. It divides the heap into equal-sized regions and prioritizes collecting the regions with the most garbage first.

Enable and tune:

# G1GC is default, but explicit flags make tuning visible java -XX:+UseG1GC \ -Xms512m -Xmx2g \ -XX:MaxGCPauseMillis=200 \ -XX:G1HeapRegionSize=16m \ -XX:G1NewSizePercent=30 \ -XX:G1MaxNewSizePercent=40 \ -jar app.jar

application.properties equivalent (Spring Boot):

spring.jvm.args=-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m

Key settings:

  • -XX:MaxGCPauseMillis=200 - G1 targets this pause goal (soft target, not guaranteed)
  • -XX:G1HeapRegionSize - set to 1-32MB; larger heaps benefit from larger regions
  • -XX:G1NewSizePercent / -XX:G1MaxNewSizePercent - control young generation size

When G1GC struggles:

  • Heaps > 16GB with latency requirements below 50ms
  • Applications with very large objects (humongous allocations > region size)

ZGC: Sub-Millisecond Pauses at Scale

ZGC (Z Garbage Collector) is a concurrent, scalable GC that keeps pauses under 1ms regardless of heap size. It's production-ready since Java 15 and generational since Java 21.

Enable ZGC:

java -XX:+UseZGC \ -XX:+ZGenerational \ -Xms1g -Xmx8g \ -XX:SoftMaxHeapSize=6g \ -jar app.jar

Java 21 - always use Generational ZGC:

# Generational ZGC is significantly faster in Java 21 -XX:+UseZGC -XX:+ZGenerational

Key settings:

  • -XX:SoftMaxHeapSize - ZGC will try to keep heap below this; helps with container memory limits
  • -XX:ZCollectionInterval=5 - force GC every 5 seconds for memory-sensitive apps
  • -XX:ZUncommitDelay=300 - return unused memory to OS after 300s

ZGC trade-off: Higher CPU usage than G1GC (concurrent work uses CPU during application execution). On CPU-constrained containers, this can reduce throughput.


Shenandoah: Low Latency on Moderate Heaps

Shenandoah is RedHat's contribution to OpenJDK. Like ZGC, it aims for sub-millisecond pauses, but uses a different algorithm (Brooks pointers for concurrent compaction) that works well on 1-16GB heaps.

java -XX:+UseShenandoahGC \ -XX:ShenandoahGCHeuristics=adaptive \ -Xms512m -Xmx4g \ -jar app.jar

Heuristics modes:

  • adaptive (default) - adjusts GC frequency based on allocation rate
  • aggressive - GC runs constantly; lowest latency, highest CPU cost
  • compact - optimizes for minimum memory footprint

Choosing the Right GC for Your Workload

Spring Boot API (heap < 4GB, mixed workload)
  ? G1GC with -XX:MaxGCPauseMillis=100

High-throughput service (heap 4-16GB, can tolerate 100ms pauses)
  ? G1GC with larger region size

Low-latency API (p99 < 10ms required, heap up to 32GB)
  ? ZGC with -XX:+ZGenerational (Java 21)

Low-latency on a budget (heap 1-8GB, moderate CPU)
  ? Shenandoah with adaptive heuristics

Batch processing (throughput > latency)
  ? ParallelGC (-XX:+UseParallelGC)

Monitoring GC in Production

Always enable GC logging - it's low overhead and essential for tuning:

java -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m \ -XX:+UseZGC \ -jar app.jar

Spring Boot Actuator exposes GC metrics via Micrometer:

management.endpoints.web.exposure.include=metrics,prometheus

Key metrics to watch:

MetricAlert
jvm.gc.pause (p99)> your latency SLA
jvm.gc.live.data.sizeApproaching max heap
jvm.memory.used (heap)> 80% sustained
jvm.gc.memory.promotedHigh rate = old gen pressure

Common Mistakes to Avoid

  • Setting -Xms much lower than -Xmx - forces the JVM to resize the heap repeatedly at startup; set them equal for predictable performance
  • Using ZGC without ZGenerational in Java 21 - non-generational ZGC is legacy; always add -XX:+ZGenerational
  • Tuning GC without measuring first - enable GC logging and look at actual pause times before changing anything; most tuning problems are actually heap sizing problems
  • Ignoring container memory limits - without -XX:+UseContainerSupport (default on Java 11+), the JVM reads host memory, not container limits, and gets OOM-killed

Summary

For most Spring Boot apps, G1GC with MaxGCPauseMillis=100-200 is the right choice. If you need sub-millisecond pauses on a larger heap, switch to ZGC with ZGenerational in Java 21. Shenandoah is a strong alternative for moderate-heap low-latency workloads. Always measure with GC logging before tuning.


Find GC and Memory Issues in Your Codebase

JOptimize analyzes your Spring Boot project for allocation hotspots, object retention patterns, and JVM configuration anti-patterns that cause GC pressure.

Identify GC bottlenecks before they cause latency spikes in production - free scan, no configuration required.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.