Java's garbage collection automatically manages memory by reclaiming memory occupied by objects that are no longer in use. Here's a comprehensive overview of GC algorithms in Java.
1. GC Generations and Heap Structure
public class HeapStructure {
public static void main(String[] args) {
// JVM Heap is divided into generations:
// - Young Generation (Eden + Survivor Spaces)
// - Old Generation (Tenured)
// - Permanent Generation / Metaspace (Java 8+)
Runtime runtime = Runtime.getRuntime();
System.out.println("Max Memory: " + runtime.maxMemory() / (1024 * 1024) + " MB");
System.out.println("Total Memory: " + runtime.totalMemory() / (1024 * 1024) + " MB");
System.out.println("Free Memory: " + runtime.freeMemory() / (1024 * 1024) + " MB");
// Object allocation demo
for (int i = 0; i < 100000; i++) {
new Object(); // Most will die young
}
System.gc(); // Suggest GC (not guaranteed)
}
}
2. Mark and Sweep Algorithm
Concept: The fundamental GC algorithm used by most collectors
public class MarkSweepDemo {
// Mark Phase: Identify live objects
// Sweep Phase: Reclaim memory from dead objects
public static void main(String[] args) {
Object obj1 = new Object(); // Live reference
Object obj2 = new Object(); // Live reference
// Create circular reference
obj1 = obj2;
obj2 = obj1;
// Even with circular references, GC can collect them
// when they become unreachable from GC roots
obj1 = null;
obj2 = null;
System.gc(); // Mark-Sweep will collect these
}
}
// GC Roots include:
// - Local variables in active methods
// - Static variables
// - Active threads
// - JNI references
3. Serial Garbage Collector
Use Case: Single-threaded, suitable for small applications
public class SerialGCDemo {
// Enable with: -XX:+UseSerialGC
public static void createGarbage() {
List<byte[]> garbage = new ArrayList<>();
for (int i = 0; i < 100; i++) {
garbage.add(new byte[1024 * 1024]); // 1MB each
if (i % 10 == 0) {
garbage.clear(); // Create some garbage
}
}
}
public static void main(String[] args) {
System.out.println("Serial GC Demo");
for (int i = 0; i < 5; i++) {
createGarbage();
System.gc();
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
4. Parallel Garbage Collector (Throughput Collector)
Use Case: Multi-threaded, suitable for batch processing
public class ParallelGCDemo {
// Enable with: -XX:+UseParallelGC
// Tuning options:
// -XX:ParallelGCThreads=N
// -XX:MaxGCPauseMillis=N
// -XX:GCTimeRatio=N
public static void main(String[] args) {
System.out.println("Parallel GC Demo");
// Create memory pressure
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
byte[] data = new byte[1024 * 100]; // 100KB
// Short-lived object
});
}
executor.shutdown();
}
}
5. CMS (Concurrent Mark Sweep) Collector
Use Case: Low pause times, suitable for responsive applications
public class CMSGCDemo {
// Enable with: -XX:+UseConcMarkSweepGC
// Deprecated in Java 9, removed in Java 14
public static void main(String[] args) {
System.out.println("CMS GC Demo - Responsive Application Simulation");
// Simulate application that needs low GC pauses
Thread uiThread = new Thread(() -> {
while (true) {
// Simulate UI rendering
renderFrame();
try {
Thread.sleep(16); // ~60 FPS
} catch (InterruptedException e) {
break;
}
}
});
Thread dataThread = new Thread(() -> {
while (true) {
// Simulate data processing creating garbage
processData();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
});
uiThread.start();
dataThread.start();
}
private static void renderFrame() {
// Simulate UI rendering - sensitive to pauses
Object[] frameData = new Object[1000];
// Short-lived objects
}
private static void processData() {
// Simulate data processing - creates garbage
List<byte[]> tempData = new ArrayList<>();
for (int i = 0; i < 100; i++) {
tempData.add(new byte[1024]); // 1KB objects
}
}
}
6. G1 (Garbage First) Collector
Use Case: Balanced throughput and pause times, default since Java 9
public class G1GCDemo {
// Enable with: -XX:+UseG1GC (default since Java 9)
// Tuning options:
// -XX:MaxGCPauseMillis=200
// -XX:G1HeapRegionSize=N
public static void main(String[] args) {
System.out.println("G1 GC Demo");
// G1 divides heap into equal-sized regions
// Collects regions with most garbage first
// Mixed collections include both young and old regions
simulateMixedWorkload();
}
private static void simulateMixedWorkload() {
// Mix of short-lived and long-lived objects
List<Object> longLived = new ArrayList<>();
List<Object> shortLived = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
// Some objects survive longer
if (i % 100 == 0) {
longLived.add(new byte[1024 * 10]); // 10KB
}
// Many short-lived objects
shortLived.add(new byte[1024]); // 1KB
if (shortLived.size() > 1000) {
shortLived.clear(); // Create garbage
}
}
}
}
7. ZGC (Z Garbage Collector)
Use Case: Very low pause times (<10ms), scalable
public class ZGCDemo {
// Enable with: -XX:+UseZGC
// Requires: Java 11+ (production since Java 15)
public static void main(String[] args) {
System.out.println("ZGC Demo - Large Heap, Low Pause");
// ZGC can handle very large heaps with minimal pause times
final long HEAP_SIZE = 16L * 1024 * 1024 * 1024; // 16GB
// Simulate large memory allocation
List<byte[]> largeData = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
largeData.add(new byte[1024 * 1024]); // 1MB objects
if (i % 100 == 0) {
// Concurrent processing while GC runs
processConcurrently();
}
}
}
private static void processConcurrently() {
// ZGC does most work concurrently with application
Thread processor = new Thread(() -> {
byte[] data = new byte[1024 * 1024]; // 1MB
// Process data while GC might be running
});
processor.start();
}
}
8. Shenandoah GC
Use Case: Low pause times, concurrent compaction
public class ShenandoahGCDemo {
// Enable with: -XX:+UseShenandoahGC
public static void main(String[] args) {
System.out.println("Shenandoah GC Demo - Concurrent Compaction");
// Shenandoah does compaction concurrently with application
simulateMemoryFragmentation();
}
private static void simulateMemoryFragmentation() {
// Create alternating pattern of different sized objects
List<Object> fragmentedHeap = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
if (i % 2 == 0) {
fragmentedHeap.add(new byte[1024 * 64]); // 64KB
} else {
fragmentedHeap.add(new byte[1024 * 16]); // 16KB
}
// Remove some objects to create fragmentation
if (i % 10 == 0) {
fragmentedHeap.remove(fragmentedHeap.size() - 1);
}
}
}
}
9. GC Monitoring and Analysis
public class GCMonitoring {
public static void main(String[] args) throws InterruptedException {
// Enable GC logging: -Xlog:gc*
// Or: -XX:+PrintGCDetails -XX:+PrintGCDateStamps
System.out.println("Monitoring GC Behavior");
// Track memory usage
Runtime runtime = Runtime.getRuntime();
Thread allocator = new Thread(() -> {
List<byte[]> objects = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
objects.add(new byte[1024 * 512]); // 512KB
// Print memory info periodically
if (i % 100 == 0) {
printMemoryStats(runtime, i);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
break;
}
}
});
allocator.start();
allocator.join();
}
private static void printMemoryStats(Runtime runtime, int iteration) {
long max = runtime.maxMemory();
long total = runtime.totalMemory();
long free = runtime.freeMemory();
long used = total - free;
System.out.printf("Iteration %d: Used=%dMB, Free=%dMB, Total=%dMB, Max=%dMB%n",
iteration,
used / (1024 * 1024),
free / (1024 * 1024),
total / (1024 * 1024),
max / (1024 * 1024));
}
}
10. GC Tuning Examples
public class GCTuningExamples {
// Common JVM flags for different scenarios:
// 1. Throughput optimization
// -XX:+UseParallelGC -XX:ParallelGCThreads=4 -Xmx2g -Xms2g
// 2. Low latency optimization
// -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx4g
// 3. Very large heap with low pauses
// -XX:+UseZGC -Xmx16g -Xms16g
// 4. Small application
// -XX:+UseSerialGC -Xmx512m
public static void simulateWorkload(String workloadType) {
switch (workloadType) {
case "throughput":
// Batch processing - favor throughput
for (int i = 0; i < 1000000; i++) {
processBatchItem(i);
}
break;
case "low-latency":
// Real-time processing - favor low pauses
for (int i = 0; i < 1000; i++) {
processRealTimeItem(i);
try { Thread.sleep(1); } catch (InterruptedException e) {}
}
break;
case "memory-intensive":
// Large memory usage
List<byte[]> largeData = new ArrayList<>();
for (int i = 0; i < 100; i++) {
largeData.add(new byte[1024 * 1024 * 10]); // 10MB
}
break;
}
}
private static void processBatchItem(int i) {
// Create some short-lived objects
byte[] data = new byte[1024];
// Process and discard
}
private static void processRealTimeItem(int i) {
// Minimal object creation to avoid GC during processing
// Reuse objects when possible
}
}
11. Memory Leak Detection
public class MemoryLeakDetection {
private static final List<Object> LEAK = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
System.out.println("Memory Leak Detection Demo");
// Simulate memory leak
Thread leakyThread = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
LEAK.add(new byte[1024 * 10]); // 10KB - never cleared
try {
Thread.sleep(1);
} catch (InterruptedException e) {
break;
}
}
});
leakyThread.start();
Thread.sleep(5000); // Let it run
// Force GC to see if memory is reclaimed
System.gc();
Thread.sleep(1000);
System.out.println("Leak list size: " + LEAK.size());
System.out.println("Memory not reclaimed due to static reference");
}
}
Key Points Summary
- Serial GC: Single-threaded, good for small apps
- Parallel GC: Multi-threaded, good for throughput
- CMS: Low pause, deprecated
- G1: Balanced, default since Java 9
- ZGC: Very low pause, large heaps
- Shenandoah: Low pause, concurrent compaction
Choose based on your application's requirements for throughput vs latency, and heap size considerations.