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
- Pause Mark Start (sub-millisecond)
- Concurrent Marking (while app runs)
- Pause Mark End (sub-millisecond)
- Concurrent Relocation (while app runs)
- 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
- Initial Mark (short pause)
- Concurrent Marking
- Final Mark (short pause)
- Concurrent Cleanup
- Concurrent Evacuation (unique to Shenandoah)
- 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
| Feature | ZGC | Shenandoah | G1 GC |
|---|---|---|---|
| Max Pause Time | 1-2ms | 10-50ms | 100-200ms+ |
| Throughput | Lower (~85% of G1) | Medium (~90% of G1) | High (Baseline) |
| Heap Size | Multi-TB | Multi-TB | Up to ~100GB |
| Concurrent Compaction | ✅ | ✅ | ❌ (needs pauses) |
| Memory Overhead | Low (2-3%) | Medium (5-10%) | Low (2-5%) |
| JDK Version | 15+ (production) | 12+ (production) | 7+ (stable) |
| Platform Support | Linux, macOS, Windows | Linux, Windows | All 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
- 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
- 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.