🔧 Java GC Tuning
Overview 🎯
Java Garbage Collection (GC) tuning is the process of optimizing the automatic memory management system to achieve better application performance. It involves adjusting various parameters and choosing appropriate GC algorithms based on application requirements.
Real-World Analogy
Think of GC tuning like optimizing a city's waste management system:
- Garbage Collectors: Different types of collection trucks (G1, ZGC, Parallel)
- Heap Size: Size of landfill/recycling centers
- Generation Sizes: Different waste processing zones
- Collection Frequency: How often trucks collect garbage
- Stop-the-World: Like temporarily closing streets for cleaning
Key Concepts 🔑
Core Components
-
Garbage Collectors
- Serial GC
- Parallel GC
- G1GC (Garbage First)
- ZGC (Z Garbage Collector)
- Shenandoah GC
-
Memory Regions
- Young Generation (Eden + Survivor)
- Old Generation
- Metaspace
-
GC Parameters
- Heap Size (-Xms, -Xmx)
- Generation Sizes (-Xmn, -XX:NewRatio)
- GC Algorithm Selection
- GC Thread Count
Implementation Examples 💻
Basic GC Configuration
- Java
- Go
public class GCConfig {
public static void main(String[] args) {
// JVM arguments for GC configuration
// -XX:+UseG1GC
// -Xms4g -Xmx4g
// -XX:MaxGCPauseMillis=200
// -XX:ParallelGCThreads=4
// -XX:ConcGCThreads=2
Runtime runtime = Runtime.getRuntime();
System.out.printf("""
Memory Configuration:
Max Memory: %d MB
Total Memory: %d MB
Free Memory: %d MB
Processors: %d%n""",
runtime.maxMemory() / 1024 / 1024,
runtime.totalMemory() / 1024 / 1024,
runtime.freeMemory() / 1024 / 1024,
runtime.availableProcessors()
);
}
}
public class GCMonitoring {
private static final Logger logger =
LoggerFactory.getLogger(GCMonitoring.class);
public static void monitorGC() {
List<GarbageCollectorMXBean> gcBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcBean : gcBeans) {
logger.info("""
GC Name: {}
Collection Count: {}
Collection Time: {} ms""",
gcBean.getName(),
gcBean.getCollectionCount(),
gcBean.getCollectionTime()
);
}
}
}
package main
import (
"fmt"
"runtime"
)
type GCConfig struct {
// Go's GC is self-tuning, but we can influence it
gcPercent int
}
func NewGCConfig() *GCConfig {
return &GCConfig{
gcPercent: runtime.GOGC,
}
}
func (gc *GCConfig) MonitorMemory() {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
fmt.Printf("Memory Statistics:\n")
fmt.Printf("Allocated: %v MB\n", mem.Alloc/1024/1024)
fmt.Printf("Total Allocated: %v MB\n", mem.TotalAlloc/1024/1024)
fmt.Printf("System Memory: %v MB\n", mem.Sys/1024/1024)
fmt.Printf("GC Cycles: %v\n", mem.NumGC)
}
func (gc *GCConfig) SetGCPercent(percent int) {
gc.gcPercent = percent
runtime.SetGCPercent(percent)
}
Advanced GC Tuning
- Java
- Go
import java.lang.management.*;
import javax.management.*;
public class AdvancedGCTuning {
private final MemoryMXBean memoryBean;
private final List<GarbageCollectorMXBean> gcBeans;
private final MemoryPoolMXBean youngGen;
private final MemoryPoolMXBean oldGen;
public AdvancedGCTuning() {
this.memoryBean = ManagementFactory.getMemoryMXBean();
this.gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
// Find memory pools
List<MemoryPoolMXBean> pools =
ManagementFactory.getMemoryPoolMXBeans();
MemoryPoolMXBean yg = null;
MemoryPoolMXBean og = null;
for (MemoryPoolMXBean pool : pools) {
if (pool.getName().contains("Young")) {
yg = pool;
} else if (pool.getName().contains("Old")) {
og = pool;
}
}
this.youngGen = yg;
this.oldGen = og;
}
public void setupMemoryMonitoring() {
// Set usage threshold for Old Gen
if (oldGen != null && oldGen.isUsageThresholdSupported()) {
long maxMemory = oldGen.getUsage().getMax();
oldGen.setUsageThreshold((long)(maxMemory * 0.8));
NotificationEmitter emitter = (NotificationEmitter)
ManagementFactory.getMemoryMXBean();
emitter.addNotificationListener(
(notification, handback) -> {
if (notification.getType().equals(
MemoryNotificationInfo
.MEMORY_THRESHOLD_EXCEEDED
)) {
handleMemoryThresholdExceeded();
}
},
null,
null
);
}
}
private void handleMemoryThresholdExceeded() {
System.gc(); // Suggest full GC
analyzeMemoryUsage();
}
public void analyzeMemoryUsage() {
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
System.out.printf("""
Memory Analysis:
Used: %d MB
Committed: %d MB
Max: %d MB
Usage: %.2f%%%n""",
heapUsage.getUsed() / 1024 / 1024,
heapUsage.getCommitted() / 1024 / 1024,
heapUsage.getMax() / 1024 / 1024,
(double) heapUsage.getUsed() / heapUsage.getMax() * 100
);
for (GarbageCollectorMXBean gcBean : gcBeans) {
System.out.printf("""
GC %s:
Count: %d
Time: %d ms
Average: %.2f ms%n""",
gcBean.getName(),
gcBean.getCollectionCount(),
gcBean.getCollectionTime(),
gcBean.getCollectionCount() > 0
? (double) gcBean.getCollectionTime() /
gcBean.getCollectionCount()
: 0
);
}
}
}
package main
import (
"fmt"
"runtime"
"runtime/debug"
"time"
)
type AdvancedGCTuning struct {
memStats runtime.MemStats
gcPauses []time.Duration
maxPauseTime time.Duration
}
func NewAdvancedGCTuning() *AdvancedGCTuning {
return &AdvancedGCTuning{
gcPauses: make([]time.Duration, 0),
}
}
func (t *AdvancedGCTuning) StartMonitoring() {
// Configure GC
debug.SetGCPercent(100) // Default 100
debug.SetMaxStack(1 * 1024 * 1024) // 1MB
go func() {
for {
t.collectMetrics()
time.Sleep(time.Second)
}
}()
}
func (t *AdvancedGCTuning) collectMetrics() {
runtime.ReadMemStats(&t.memStats)
fmt.Printf("GC Statistics:\n")
fmt.Printf("Number of GCs: %d\n", t.memStats.NumGC)
fmt.Printf("Heap Size: %d MB\n", t.memStats.HeapAlloc/1024/1024)
fmt.Printf("Next GC Target: %d MB\n", t.memStats.NextGC/1024/1024)
// Analyze last GC pause
if t.memStats.NumGC > 0 {
pause := t.memStats.PauseNs[(t.memStats.NumGC+255)%256]
pauseDuration := time.Duration(pause)
t.gcPauses = append(t.gcPauses, pauseDuration)
if pauseDuration > t.maxPauseTime {
t.maxPauseTime = pauseDuration
}
fmt.Printf("Last GC Pause: %v\n", pauseDuration)
fmt.Printf("Max GC Pause: %v\n", t.maxPauseTime)
}
}
Best Practices 🌟
GC Configuration
- Heap Size Settings
# Consistent heap size
-Xms4g -Xmx4g
# Young generation size
-XX:NewSize=2g -XX:MaxNewSize=2g
# Metaspace size
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
- G1GC Configuration
# Use G1GC
-XX:+UseG1GC
# Target pause time
-XX:MaxGCPauseMillis=200
# Region size
-XX:G1HeapRegionSize=16m
# Reserve memory
-XX:G1ReservePercent=10
Monitoring
- GC Logging
# Enable GC logging
-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=100m
# Detailed logging
-Xlog:gc+heap=debug
-Xlog:gc+phases=debug
- Performance Monitoring
public class GCPerformanceMonitor {
private static final long MONITORING_PERIOD = 60000; // 1 minute
public void monitor() {
ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
Collection<GarbageCollectorMXBean> gcBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcBean : gcBeans) {
long collectionCount = gcBean.getCollectionCount();
long collectionTime = gcBean.getCollectionTime();
System.out.printf(
"GC %s - Count: %d, Time: %d ms%n",
gcBean.getName(),
collectionCount,
collectionTime
);
}
}, 0, MONITORING_PERIOD, TimeUnit.MILLISECONDS);
}
}
Common Pitfalls 🚨
- Incorrect Heap Sizing
// Wrong: Different min and max heap sizes
-Xms1g -Xmx4g // Can cause performance issues
// Correct: Consistent heap size
-Xms4g -Xmx4g
- Inappropriate GC Algorithm Selection
// Wrong: Using Serial GC for high-throughput server
-XX:+UseSerialGC
// Correct: Using G1GC for server applications
-XX:+UseG1GC
Use Cases 🎯
1. High-Throughput Batch Processing
// Configuration for batch processing
-XX:+UseParallelGC
-XX:ParallelGCThreads=16
-Xms8g -Xmx8g
-XX:NewRatio=2
2. Low-Latency Trading Application
// Configuration for low latency
-XX:+UseZGC
-Xms16g -Xmx16g
-XX:ZCollectionInterval=5
-XX:ZAllocationSpikeTolerance=2
3. Web Application Server
// Configuration for web server
-XX:+UseG1GC
-Xms4g -Xmx4g
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
Deep Dive Topics 🔍
Garbage Collector Selection
- G1GC Internals
/**
* G1GC Configuration Guide
*
* Region Size = heap size / 2048
* Minimum region size = 1MB
* Maximum region size = 32MB
*
* -XX:G1HeapRegionSize=n
* -XX:MaxGCPauseMillis=200
* -XX:G1NewSizePercent=30
* -XX:G1MaxNewSizePercent=60
*/
- ZGC Configuration
/**
* ZGC Configuration Guide
*
* -XX:+UseZGC
* -XX:ZCollectionInterval=n
* -XX:ZAllocationSpikeTolerance=n
* -XX:ZUncommit=true/false
*/
Performance Analysis
public class GCAnalyzer {
private final Map<String, Long> gcTimings = new ConcurrentHashMap<>();
private final Map<String, Long> gcCounts = new ConcurrentHashMap<>();
public void analyze() {
for (GarbageCollectorMXBean gc :
ManagementFactory.getGarbageCollectorMXBeans()
) {
long count = gc.getCollectionCount();
long time = gc.getCollectionTime();
gcCounts.put(gc.getName(), count);
gcTimings.put(gc.getName(), time);
double avgTime = count > 0 ? (double) time / count : 0;
System.out.printf(
"GC %s: %d collections, avg time %.2f ms%n",
gc.getName(),
count,
avgTime
);
}
}
}