The JVM’s garbage collectors aren’t just about freeing memory; they’re about fundamentally controlling how your application’s threads interact with the heap.
Let’s see G1 in action. Imagine a simple application that continuously creates objects:
import java.util.ArrayList;
import java.util.List;
public class HeapSpammer {
private static final int OBJECT_SIZE = 1024; // 1KB object
private static final int NUM_OBJECTS = 100000; // 100,000 objects
public static void main(String[] args) throws InterruptedException {
List<byte[]> heap = new ArrayList<>();
System.out.println("Starting heap spamming...");
while (true) {
byte[] data = new byte[OBJECT_SIZE];
heap.add(data);
if (heap.size() > NUM_OBJECTS) {
// Simulate some churn by removing older objects
heap.remove(0);
}
// Small delay to allow GC to potentially kick in
if (System.currentTimeMillis() % 1000 == 0) {
Thread.sleep(1);
}
}
}
}
When you run this with a typical G1 configuration, you’ll observe pauses. The JVM might start with a heap size of, say, 4GB. As HeapSpammer fills it, G1 will eventually trigger a "Young GC" or "Mixed GC." During these pauses, application threads stop. You can see this with GC logging enabled (e.g., -Xlog:gc*).
The core problem G1 solves is managing large heaps without excessively long pauses. It divides the heap into regions and tracks object occupancy and age. Instead of a full heap scan, it intelligently selects regions to collect based on their "garbage-first" potential and garbage collection "thresholds." It aims to meet a pause time goal (e.g., -XX:MaxGCPauseMillis=200) by doing incremental work during the application’s live time, but it still requires "stop-the-world" phases for certain operations, especially during full GCs or when the evacuation pause exceeds its target.
The levers you control with G1 are primarily:
-Xms<size>and-Xmx<size>: The initial and maximum heap size.-XX:MaxGCPauseMillis=<ms>: The target pause time. G1 tries to achieve this by adjusting the amount of work it does per GC cycle.-XX:NewRatio=<ratio>: Controls the relative size of the young generation to the old generation.-XX:G1HeapRegionSize=<size>: The size of individual regions (usually auto-tuned).
The full mental model involves understanding the Young Generation (Eden, Survivor spaces) and Old Generation. G1 manages these as a set of regions. Objects are born in Eden. When Eden fills, a Young GC occurs, promoting surviving objects to Survivor spaces or the Old Generation. As the Old Generation fills, Mixed GCs occur, collecting garbage from both Young and Old Generation regions.
What most people don’t realize is that G1’s pause time goal is just that: a goal. When the heap becomes heavily fragmented or when a significant portion of the Old Generation is occupied by long-lived objects that are difficult to reclaim, G1 might need to perform a "Full GC." This is a stop-the-world pause that scans the entire heap. While less frequent than in older collectors, it can still cause significant latency if it happens.
The next step in understanding JVM memory management is exploring collectors designed for even lower latencies, like ZGC and Shenandoah.