The Role of GC and Key Concepts
Garbage Collection (GC) is a JVM mechanism that automatically reclaims memory from objects that are no longer referenced. The goal of GC tuning is to find the optimal balance between throughput and latency.
Think of it like a library. The GC is the librarian who shelves returned books. While the librarian is organizing, lending (the application) pauses briefly — that is the Stop-the-World (STW) pause.
JVM Heap Memory Structure
| Region | Description | Characteristics |
|---|---|---|
| Young Generation | Newly created objects | Eden + Survivor 0/1 |
| Old Generation | Long-surviving objects | Objects promoted from Minor GC |
| Metaspace | Class metadata | Replaced PermGen in Java 8+, native memory |
GC Algorithm Comparison
// GcComparison.java — Comparing characteristics by GC algorithm
public class GcComparison {
public static void main(String[] args) {
// GC algorithm selection guide
//
// 1. G1 GC (default since Java 9+)
// Run: java -XX:+UseG1GC -Xmx4g -jar app.jar
// Features: Divides heap into equal-sized regions, collects garbage-heavy regions first
// Goal: Keep STW pauses under 200ms
// Best for: Heap 4GB+, general server applications
//
// 2. ZGC (Java 15+, Generational ZGC in Java 21)
// Run: java -XX:+UseZGC -Xmx16g -jar app.jar
// Features: Colored pointers + load barriers, mostly concurrent
// Goal: STW pauses under 1ms (regardless of heap size)
// Best for: Low-latency systems (finance, real-time processing)
//
// 3. Shenandoah GC
// Run: java -XX:+UseShenandoahGC -Xmx8g -jar app.jar
// Features: Concurrent compaction, low-latency goal similar to ZGC
// Best for: Low-latency needs in OpenJDK environments
//
// 4. Parallel GC
// Run: java -XX:+UseParallelGC -Xmx4g -jar app.jar
// Features: GC with multiple threads, maximizes throughput
// Best for: Batch processing, when pauses are acceptable
// Check current JVM's GC info
var gcBeans = java.lang.management.ManagementFactory.getGarbageCollectorMXBeans();
for (var gc : gcBeans) {
System.out.printf("GC name: %s, Collection count: %d, Total time: %dms%n",
gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
}
// Output (G1 GC):
// GC name: G1 Young Generation, Collection count: 3, Total time: 15ms
// GC name: G1 Old Generation, Collection count: 0, Total time: 0ms
}
}
G1 GC Tuning
// G1GcTuning.java — G1 GC key options and heap analysis
public class G1GcTuning {
public static void main(String[] args) {
// G1 GC key JVM options
//
// Heap size settings
// -Xms4g -Xmx4g // Set min/max heap equal (prevents GC heap resizing)
//
// G1 target pause time
// -XX:MaxGCPauseMillis=200 // Target STW time (default 200ms)
//
// Region size (1-32MB, power of 2)
// -XX:G1HeapRegionSize=4m // Increase for many large objects
//
// Humongous object threshold
// Objects 50%+ of region size -> allocated to Humongous regions
// Increase region size if large arrays are frequent
//
// Enable GC logging (Java 9+)
// -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=10m
// Memory usage monitoring code
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory() / (1024 * 1024); // Max heap
long totalMemory = runtime.totalMemory() / (1024 * 1024); // Current allocated heap
long freeMemory = runtime.freeMemory() / (1024 * 1024); // Unused heap
long usedMemory = totalMemory - freeMemory; // Actual usage
System.out.printf("Max heap: %dMB%n", maxMemory);
System.out.printf("Current heap: %dMB%n", totalMemory);
System.out.printf("Used: %dMB%n", usedMemory);
System.out.printf("Free: %dMB%n", freeMemory);
System.out.printf("Usage: %.1f%%%n", (double) usedMemory / maxMemory * 100);
// Output:
// Max heap: 4096MB
// Current heap: 256MB
// Used: 12MB
// Free: 244MB
// Usage: 0.3%
// Force GC (never use in production, for analysis only)
System.gc();
}
}
ZGC Usage and Configuration
// ZgcDemo.java — ZGC characteristics and memory allocation test
import java.util.ArrayList;
import java.util.List;
public class ZgcDemo {
public static void main(String[] args) {
// ZGC run options
// java -XX:+UseZGC -XX:+ZGenerational -Xmx8g ZgcDemo
//
// Java 21+ Generational ZGC (enabled by default)
// -XX:+UseZGC // Enable ZGC
// -XX:+ZGenerational // Generational ZGC (Java 21+)
// -XX:SoftMaxHeapSize=4g // Soft heap upper limit (try to stay below)
// -XX:ConcGCThreads=4 // Concurrent GC thread count
//
// ZGC characteristics:
// - STW pauses under 1ms regardless of heap size
// - More generous heap size = better GC efficiency
// - Memory overhead: ~3-5% (for colored pointers)
// Memory pressure simulation
List<byte[]> allocations = new ArrayList<>();
long startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
allocations.add(new byte[1024 * 1024]); // Allocate 1MB
if (i % 100 == 0) {
// Release half of old allocations -> GC targets
int removeCount = allocations.size() / 2;
for (int j = 0; j < removeCount; j++) {
allocations.remove(0);
}
long elapsed = (System.nanoTime() - startTime) / 1_000_000;
Runtime rt = Runtime.getRuntime();
long used = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
System.out.printf("[%4dms] Allocation %d, Used memory: %dMB%n",
elapsed, i, used);
}
}
// Output (ZGC):
// [ 5ms] Allocation 0, Used memory: 25MB
// [ 45ms] Allocation 100, Used memory: 312MB
// [ 82ms] Allocation 200, Used memory: 289MB
// [120ms] Allocation 300, Used memory: 301MB
// ...
// ZGC: Stable memory management with no pauses
}
}
GC Analysis with JFR (Java Flight Recorder)
// JfrGcAnalysis.java — Analyzing GC behavior with JFR events
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordingFile;
import java.nio.file.Path;
import java.util.DoubleSummaryStatistics;
import java.util.ArrayList;
import java.util.List;
public class JfrGcAnalysis {
public static void main(String[] args) throws Exception {
// How to start JFR recording:
// 1. JVM option: java -XX:StartFlightRecording=duration=60s,filename=gc.jfr -jar app.jar
// 2. jcmd: jcmd <PID> JFR.start duration=60s filename=gc.jfr
// JFR file analysis (assuming gc.jfr exists)
Path jfrFile = Path.of("gc.jfr");
List<Double> pauseTimes = new ArrayList<>();
try (RecordingFile recording = new RecordingFile(jfrFile)) {
while (recording.hasMoreEvents()) {
RecordedEvent event = recording.readEvent();
// Filter GC pause events
if (event.getEventType().getName().equals("jdk.GCPhasePause")) {
double pauseMs = event.getDuration().toNanos() / 1_000_000.0;
String gcName = event.getString("name");
pauseTimes.add(pauseMs);
System.out.printf("GC pause: %s, Duration: %.2fms%n", gcName, pauseMs);
}
// Heap usage events
if (event.getEventType().getName().equals("jdk.GCHeapSummary")) {
long heapUsed = event.getLong("heapUsed") / (1024 * 1024);
System.out.printf("Heap used: %dMB%n", heapUsed);
}
}
}
// Statistics summary
if (!pauseTimes.isEmpty()) {
DoubleSummaryStatistics stats = pauseTimes.stream()
.mapToDouble(Double::doubleValue)
.summaryStatistics();
System.out.println("\n=== GC Pause Statistics ===");
System.out.printf("Total count: %d%n", stats.getCount());
System.out.printf("Average: %.2fms%n", stats.getAverage());
System.out.printf("Maximum: %.2fms%n", stats.getMax());
System.out.printf("Minimum: %.2fms%n", stats.getMin());
}
// Example output:
// GC pause: G1 Young, Duration: 8.34ms
// Heap used: 512MB
// GC pause: G1 Young, Duration: 5.21ms
// ...
// === GC Pause Statistics ===
// Total count: 47
// Average: 6.82ms
// Maximum: 15.43ms
// Minimum: 2.11ms
}
}
Memory Leak Diagnosis
// MemoryLeakDetection.java — Common memory leak patterns and diagnosis
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.HashMap;
import java.util.Map;
public class MemoryLeakDetection {
// Memory leak pattern 1: Data accumulation in static maps
// private static final Map<String, byte[]> CACHE = new HashMap<>();
// -> Solution: Use WeakHashMap or size-limited cache (Caffeine, Guava Cache)
// Memory leak pattern 2: Unregistered listeners/callbacks
// eventEmitter.addListener(this);
// -> Solution: Call removeListener(), use WeakReference
// Memory leak pattern 3: Unclosed connections/streams
// Connection conn = dataSource.getConnection();
// -> Solution: Use try-with-resources
public static void main(String[] args) {
// Check current memory state
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
System.out.printf("Heap init: %dMB%n", heapUsage.getInit() / (1024 * 1024));
System.out.printf("Heap used: %dMB%n", heapUsage.getUsed() / (1024 * 1024));
System.out.printf("Heap committed: %dMB%n", heapUsage.getCommitted() / (1024 * 1024));
System.out.printf("Heap max: %dMB%n", heapUsage.getMax() / (1024 * 1024));
// Output:
// Heap init: 256MB
// Heap used: 8MB
// Heap committed: 256MB
// Heap max: 4096MB
// Generate heap dump (automatic on OutOfMemoryError)
// JVM option: -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=/tmp/heapdump.hprof
//
// Manual dump: jcmd <PID> GC.heap_dump /tmp/heapdump.hprof
// Analysis tools: Eclipse MAT, VisualVM, JProfiler
//
// Diagnosis steps:
// 1. jcmd <PID> GC.heap_info -> Check per-region heap usage
// 2. jcmd <PID> GC.class_histogram -> Top object count/size list
// 3. Heap dump -> Leak Suspects analysis in MAT
}
}
Summary
GC tuning should follow a “Measure, Analyze, Adjust, Verify” cycle.
- GC selection criteria: G1 (default) for general servers, ZGC for low-latency systems, Parallel GC for batch processing
- Heap size: Set
-Xmsand-Xmxto the same value to eliminate heap resizing overhead. Allocate 50-70% of physical memory to the heap - G1 tuning: Set target pause with
MaxGCPauseMillis, and increaseG1HeapRegionSizeif there are many large objects - ZGC: With Generational ZGC on Java 21+, achieve sub-1ms pauses regardless of heap size
- Monitoring is essential: Always enable GC logs (
-Xlog:gc*) and JFR. Tuning GC without logs in production is like driving blindfolded - Memory leak diagnosis: If Old Gen keeps growing after GC, suspect a memory leak and analyze heap dumps