Back to Blog
javagarbage-collectionperformancejvm-tuning

Java Garbage Collection Tuning: Reduce Pauses and Latency Spikes

Master GC tuning. Learn heap sizing, collector choice, and how to achieve predictable latency in production.

J

JOptimize Team

June 29, 2026· 14 min read

Garbage collection pauses kill latency SLAs. A full GC stop-the-world pause can freeze your application for 100ms-2s. Users notice every millisecond.

This guide covers GC fundamentals, tuning strategies, and production settings to achieve consistent sub-100ms latency.

The GC Problem

Young generation GC: 10-50ms pause
Old generation GC: 100ms-2s pause
Full GC: 500ms-5s pause

At 10,000 requests/sec, a 500ms pause = 5,000 dropped requests.

Heap Sizing

Heap size directly impacts GC frequency and pause time.

Too Small Heap

java -Xmx1g MyApp # Too small for high-throughput

Heap fills quickly. Frequent GC collections. Constant pauses.

Too Large Heap

java -Xmx64g MyApp # Too large

Full GC pauses are catastrophic (multi-second). Takes forever to scan entire heap.

Right Size

# 50-70% utilization at steady state java -Xmx8g MyApp # For 8-core server

Heap typically 4-16GB for production servers. Size based on peak live set + headroom.

Calculating Heap Size

Initial estimate: live_set_size × 2
With safety margin: live_set_size × 3
  1. Run app under load
  2. Measure heap usage after GC (true live set)
  3. Heap size = live_set × 2-3

Garbage Collectors

java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MyApp

Pros:

  • Predictable pause times (<200ms typical)
  • Scales to large heaps (10GB+)
  • Concurrent collection

Cons:

  • Higher CPU overhead (15-20%)
  • Tuning required for very low latency

ZGC (Sub-millisecond Pauses)

java -XX:+UseZGC -Xmx16g MyApp # Java 11+

Pros:

  • Ultra-low pauses (<10ms even on 64GB heap)
  • No generational bias
  • Scales perfectly to 16GB+ heaps

Cons:

  • High CPU overhead (30%+)
  • Requires Java 11+
  • More memory overhead than G1

Shenandoah (Low-Latency Alternative)

java -XX:+UseShenandoahGC MyApp # Java 12+

Pros:

  • Consistent sub-100ms pauses
  • Lower CPU than ZGC

Cons:

  • Less mature than ZGC
  • Fewer production deployments

G1GC Tuning

G1 is the default for most Java applications.

Set Max Pause Target

java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 MyApp

G1 tries to achieve <100ms pauses. Lower = more frequent collections.

Region Size

java -XX:+UseG1GC -XX:G1HeapRegionSize=16m MyApp

For 8GB heap, use 16MB regions. Default auto-calculated, manual tuning rarely needed.

Parallel GC Threads

java -XX:+UseG1GC -XX:ParallelGCThreads=8 MyApp

Number of threads performing GC. Default = cores/4. For latency, use cores/2.

Monitoring GC

Enable GC Logging

java -Xmx8g \ -Xlog:gc*:file=gc.log:time,level,tags \ -XX:+UseG1GC \ MyApp

Logs all GC events with timestamps.

Parse GC Logs

# Analyze pauses grep "Pause" gc.log | awk '{print $NF}' | sort -n | tail -10

Shows longest pauses. Should all be <200ms.

JVM Metrics

// Using Micrometer @Component public class GCMetrics { @PostConstruct public void setupMetrics(MeterRegistry registry) { // Track GC pause times new MemoryMetrics().bindTo(registry); List<GarbageCollectorMXBean> collectors = ManagementFactory.getGarbageCollectorMXBeans(); for (GarbageCollectorMXBean bean : collectors) { registry.gauge( "jvm.gc.pause_time", bean.getCollectionTime() ); } } }

Production Configuration

Low-Latency Application (SLA <100ms)

java -server \ -Xmx8g \ -XX:+UseZGC \ -XX:+AlwaysPreTouch \ -XX:+TieredCompilation \ -XX:C1CompileThreshold=1500 \ -Xlog:gc*:file=gc.log:time,level,tags \ MyApp

Explanation:

  • -XX:+UseZGC: Sub-10ms pauses
  • -XX:+AlwaysPreTouch: Pre-fault pages (avoid page faults during GC)
  • -XX:+TieredCompilation: Optimize startup + steady-state
  • -XX:C1CompileThreshold=1500: Compile after 1500 invocations (balance startup/performance)

Throughput-Optimized (GC pauses OK, maximize throughput)

java -server \ -Xmx16g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=100 \ -XX:+ParallelRefProcEnabled \ -Xlog:gc*:file=gc.log \ MyApp

Common Mistakes

Mistake 1: Full GC Every 30 Seconds

Cause: Heap too small

# Bad java -Xmx2g MyApp # Good: Increase heap java -Xmx8g MyApp

Mistake 2: Old Generation Fills Completely

Cause: Object leak or heap too small

// Check: Old gen should stabilize after GC // If it keeps growing, you have a leak

Mistake 3: 99th Percentile Latency Terrible

Cause: Long GC pauses

# Bad: G1 trying to keep up java -XX:+UseG1GC -XX:MaxGCPauseMillis=50 MyApp # Good: Use ZGC for predictable latency java -XX:+UseZGC MyApp

Production Checklist

  1. Measure heap usage under load
  2. Set heap size = live_set × 2-3
  3. Choose collector: G1 (default), ZGC (ultra-low latency), Shenandoah (alternative)
  4. Enable GC logging in production
  5. Set GC pause target based on SLA
  6. Monitor GC metrics (pause times, frequency)
  7. Alert on Full GC events
  8. Test under load before deploying
  9. Profile garbage allocation rate
  10. Use jstat to check GC in production

Summary

GC pauses destroy latency. Heap size matters: too small = frequent pauses, too large = catastrophic full GC.

Choose collector:

  • G1GC (default): good for most apps, predictable <200ms pauses
  • ZGC: ultra-low latency (<10ms), Java 11+
  • Shenandoah: alternative, lower CPU than ZGC

Monitor GC metrics in production. Alert on full GC events.

Achieve Consistent Latency with JOptimize

GC tuning is one part. Thread starvation, lock contention, and allocation storms also cause spikes.

JOptimize detects GC hotspots and allocation patterns:

  • IntelliJ Plugin - highlight allocation hotspots
  • JOptimize PRO - full GC and memory analysis

Use code LINKEDIN40 for 40% OFF JOptimize PRO.

Tune GC. Monitor pauses. Achieve sub-100ms latency consistently.

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.