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.
JOptimize Team
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.
| Collector | Default since | Pause model | Best for |
|---|---|---|---|
| G1GC | Java 9 | Low pause (10-200ms) | Most workloads |
| ZGC | Java 15 (prod) | Sub-millisecond | Large heaps, latency-sensitive |
| Shenandoah | Java 12 (prod) | Sub-millisecond | Low-latency, moderate heap |
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 sizeWhen G1GC struggles:
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 300sZGC trade-off: Higher CPU usage than G1GC (concurrent work uses CPU during application execution). On CPU-constrained containers, this can reduce throughput.
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 rateaggressive - GC runs constantly; lowest latency, highest CPU costcompact - optimizes for minimum memory footprintSpring 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)
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:
| Metric | Alert |
|---|---|
jvm.gc.pause (p99) | > your latency SLA |
jvm.gc.live.data.size | Approaching max heap |
jvm.memory.used (heap) | > 80% sustained |
jvm.gc.memory.promoted | High rate = old gen pressure |
-Xms much lower than -Xmx - forces the JVM to resize the heap repeatedly at startup; set them equal for predictable performanceZGenerational in Java 21 - non-generational ZGC is legacy; always add -XX:+ZGenerational-XX:+UseContainerSupport (default on Java 11+), the JVM reads host memory, not container limits, and gets OOM-killedFor 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.
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.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.