ZGC & Shenandoah: Next-Generation Garbage Collectors in Java

Introduction to Low-Pause GCs

ZGC (Z Garbage Collector) and Shenandoah are two revolutionary garbage collectors introduced in OpenJDK, designed to achieve extremely low pause times (typically under 10ms) even with very large heaps (multiple terabytes). They represent the next evolution in GC technology, addressing the limitations of traditional collectors.


1. ZGC (Z Garbage Collector)

Overview

  • Introduced in: JDK 11 (as experimental), production-ready in JDK 15
  • Primary Goal: Sub-millisecond pause times regardless of heap size
  • Heap Size: Scales from hundreds of MB to multiple TB
  • Design Philosophy: Low latency at the cost of slightly higher CPU usage

Key Features

Colored Pointers & Load Barriers

// Conceptual representation - ZGC uses pointer metadata
Object reference = 0x0000000000000000  // Normal pointer
Object reference = 0x0000000000000001  // ZGC pointer with metadata in bits
// Pointer layout:
// 0-41 bits: Object address (4TB addressable space)
// 42-44 bits: Metadata (mark, remap, finalizable)
// 45-63 bits: Unused

Concurrent Phases

  • Concurrent Marking: Identifies live objects while application runs
  • Concurrent Relocation: Moves objects while application accesses them
  • Concurrent Reference Processing: Handles soft/weak/phantom references

Architecture & Working

Memory Management

public class ZGCExample {
private static final List<byte[]> largeList = new ArrayList<>();
public static void main(String[] args) {
// ZGC handles this efficiently with minimal pauses
for (int i = 0; i < 1000000; i++) {
largeList.add(new byte[1024]); // 1KB objects
if (i % 1000 == 0) {
System.gc(); // Almost no pause with ZGC
}
}
}
}

GC Cycle Phases

  1. Pause Mark Start (sub-millisecond)
  2. Concurrent Marking (while app runs)
  3. Pause Mark End (sub-millisecond)
  4. Concurrent Relocation (while app runs)
  5. Concurrent Reference Processing

Configuration & Usage

# Enable ZGC
java -XX:+UseZGC -Xmx16g -Xms16g MyApplication
# With additional tuning
java -XX:+UseZGC -Xmx32g -Xms32g \
-XX:ConcGCThreads=4 \          # Concurrent GC threads
-XX:ParallelGCThreads=8 \      # Parallel GC threads  
-XX:ZAllocationSpikeTolerance=4.0 \ # Allocation spike tolerance
-XX:ZCollectionInterval=120 \  # Force GC interval (seconds)
MyApplication

Performance Characteristics

  • Pause Times: Typically 1-2ms, independent of heap size
  • Throughput: 5-15% lower than G1 GC due to concurrent overhead
  • Memory Overhead: ~2-3% for colored pointers and metadata
  • CPU Usage: Higher than stop-the-world collectors

2. Shenandoah GC

Overview

  • Introduced in: JDK 12 (Red Hat contribution)
  • Primary Goal: Consistent low pause times with good throughput
  • Key Innovation: Brooks pointers for concurrent compaction
  • Design Philosophy: Balance between latency and throughput

Key Features

Brooks Pointers (Forwarding Pointers)

// Conceptual object layout with Brooks pointer
class ObjectHeader {
Object forwardingPointer;  // Points to new location during relocation
// ... other header fields
}
// During concurrent compaction:
// Object A → [forwarding] → Object A' (new location)
// Application threads seamlessly follow forwarding pointers

Concurrent Compaction

  • No stop-the-world compaction pauses
  • Application continues running during object movement
  • Read/write barriers handle pointer updates transparently

Architecture & Working

GC Cycle Phases

  1. Initial Mark (short pause)
  2. Concurrent Marking
  3. Final Mark (short pause)
  4. Concurrent Cleanup
  5. Concurrent Evacuation (unique to Shenandoah)
  6. Update References (short pause)

Memory Regions

public class ShenandoahExample {
// Shenandoah divides heap into regions like G1
// But evacuates regions concurrently
public static void demonstrateAllocation() {
List<LargeObject> objects = new ArrayList<>();
// Shenandoah handles allocation spikes with minimal pauses
for (int i = 0; i < 10000; i++) {
objects.add(new LargeObject(mb(10))); // 10MB objects
}
// Concurrent evacuation happens without stopping application
objects.clear();
System.gc(); // Tiny pause for initiation only
}
private static byte[] mb(int size) {
return new byte[size * 1024 * 1024];
}
}

Configuration & Usage

# Enable Shenandoah
java -XX:+UseShenandoahGC -Xmx16g -Xms16g MyApplication
# With tuning options
java -XX:+UseShenandoahGC -Xmx32g -Xms32g \
-XX:ShenandoahGCMode=normal \        # normal, iu (interactive)
-XX:ShenandoahGCHeuristics=adaptive \ # adaptive, static, compact
-XX:ShenandoahTargetIntervalMs=100 \  # Target pause interval
-XX:ShenandoahAllocationThreshold=70 \ # Allocation spike threshold
-XX:ShenandoahUncommitDelay=1000 \    # Memory return delay (ms)
MyApplication

Performance Characteristics

  • Pause Times: Typically 10-50ms, very consistent
  • Throughput: Better than ZGC, closer to G1
  • Memory Overhead: Higher than ZGC due to Brooks pointers
  • CPU Usage: Moderate, between G1 and ZGC

3. Comparative Analysis

Feature Comparison

FeatureZGCShenandoahG1 GC
Max Pause Time1-2ms10-50ms100-200ms+
ThroughputLower (~85% of G1)Medium (~90% of G1)High (Baseline)
Heap SizeMulti-TBMulti-TBUp to ~100GB
Concurrent Compaction❌ (needs pauses)
Memory OverheadLow (2-3%)Medium (5-10%)Low (2-5%)
JDK Version15+ (production)12+ (production)7+ (stable)
Platform SupportLinux, macOS, WindowsLinux, WindowsAll platforms

Use Case Scenarios

Choose ZGC When:

// Real-time financial trading systems
public class TradingSystem {
// Cannot tolerate GC pauses > 1ms
// Large heap (64GB+) with strict latency requirements
}
// Latency-sensitive microservices
@RestController
public class PaymentService {
@PostMapping("/payments")
public PaymentResponse processPayment(@RequestBody PaymentRequest request) {
// 99.9% of requests must complete in < 10ms
// ZGC ensures consistent response times
}
}

Choose Shenandoah When:

// High-throughput, low-latency web applications
public class WebApplication {
// Need better throughput than ZGC
// Can tolerate 10-50ms pauses occasionally
// Good balance between latency and throughput
}
// Data processing applications
public class DataProcessor {
// Process large datasets in memory
// Need predictable pause times during processing
}

Stick with G1 When:

// Batch processing applications
public class BatchJob {
// Throughput is more important than latency
// Can tolerate occasional 100-200ms pauses
// Running on older JDK versions
}

4. Performance Tuning Guidelines

ZGC Tuning Strategies

# Basic production configuration
java -XX:+UseZGC -Xmx32g -Xms32g \
-XX:MaxGCPauseMillis=5 \        # Target max pause (best effort)
-XX:ConcGCThreads=6 \           # 1/4 of CPU cores
-XX:ParallelGCThreads=24 \      # All available cores
-XX:+UnlockDiagnosticVMOptions \
-XX:-ZProactive \               # Disable proactive cycles if needed
-jar myapp.jar
# Monitoring ZGC
java -XX:+UseZGC -Xlog:gc*,gc+stats=off,gc+heap=off \
-Xmx16g -jar myapp.jar

Shenandoah Tuning Strategies

# Basic production configuration  
java -XX:+UseShenandoahGC -Xmx32g -Xms32g \
-XX:ShenandoahGCMode=normal \
-XX:ShenandoahGCHeuristics=adaptive \
-XX:ShenandoahTargetIntervalMs=50 \ # Target pause interval
-XX:ShenandoahAllocationThreshold=70 \ # Start GC at 70% allocation
-XX:ShenandoahUncommitDelay=5000 \ # Return memory after 5 seconds
-jar myapp.jar
# For latency-sensitive applications
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=iu \ # Interactive mode
-XX:ShenandoahGCHeuristics=static \ # More predictable
-Xmx16g -jar myapp.jar

5. Monitoring and Diagnostics

ZGC Monitoring

# Enable detailed ZGC logging
java -XX:+UseZGC -Xlog:gc*,gc+heap=debug,gc+stats=debug \
-Xmx16g -jar myapp.jar
# Common metrics to monitor:
# - gc.zgc.cycles (total collection cycles)
# - gc.zgc.pause.time.max (maximum pause time)
# - gc.heap.used (heap utilization)

Shenandoah Monitoring

# Enable Shenandoah logging
java -XX:+UseShenandoahGC -Xlog:gc*,gc+ergo=debug,gc+phases=debug \
-Xmx16g -jar myapp.jar
# Key metrics:
# - gc.shenandoah.cycles.total
# - gc.shenandoah.pause.time.max  
# - gc.shenandoah.concurrent.time.total

6. Best Practices & Recommendations

When to Use Next-Gen GCs

  1. Adopt ZGC/Shenandoah if:
  • Your application requires consistent low latency
  • You have large heaps (> 16GB)
  • You're experiencing long GC pauses with traditional collectors
  • You can allocate extra CPU resources
  1. Stick with Traditional Collectors if:
  • Throughput is more important than latency
  • You have small heaps (< 4GB)
  • Running on older JDK versions
  • Limited CPU resources

Migration Strategy

// Before: Using G1 GC
// java -XX:+UseG1GC -Xmx8g -jar app.jar
// After: Migrating to ZGC
// 1. Test with same heap size first
// java -XX:+UseZGC -Xmx8g -jar app.jar
// 2. Monitor performance and memory usage
// 3. Adjust heap size if needed (ZGC may need slightly more heap)
// 4. Tune GC parameters based on application behavior

Common Pitfalls

  • Insufficient CPU: Both collectors need adequate CPU for concurrent phases
  • Over-optimization: Start with defaults, then tune based on metrics
  • Memory allocation patterns: Sudden allocation spikes can trigger more frequent collections
  • Monitoring gap: Ensure proper monitoring before and after migration

Summary

ZGC and Shenandoah represent the future of garbage collection in Java, offering unprecedented low-latency performance for modern applications. While they come with some CPU overhead, the benefits in predictable response times make them essential for latency-sensitive applications running on large heaps.

The choice between them often comes down to specific requirements:

  • ZGC for the absolute lowest pause times
  • Shenandoah for a better balance between latency and throughput

Both continue to evolve with each JDK release, making them increasingly suitable for production workloads.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper