The JVM heap isn’t just one big blob; it’s a strategically divided landscape designed to optimize garbage collection.
Let’s see it in action. Imagine a simple Java application that continuously creates objects:
import java.util.ArrayList;
import java.util.List;
public class HeapDemo {
public static void main(String[] args) throws InterruptedException {
List<Object> objects = new ArrayList<>();
int counter = 0;
while (true) {
objects.add(new byte[1024 * 1024]); // Add a 1MB object
counter++;
if (counter % 1000 == 0) {
System.out.println("Created " + counter + "MB of objects.");
// In a real app, objects might go out of scope here
// For this demo, we keep them to fill the heap
}
// Small sleep to prevent overwhelming the CPU for the demo
Thread.sleep(1);
}
}
}
When you run this with a JVM, you’d typically see output like:
Created 1000MB of objects.
Created 2000MB of objects.
Created 3000MB of objects.
...
As this runs, the JVM is actively managing where these byte[1024 * 1024] objects land and how they’re eventually cleaned up.
The heap is primarily divided into two main generations: the Young Generation and the Old Generation. There’s also a separate area called Metaspace (or PermGen in older JVMs).
Young Generation: This is where all new objects are allocated. It’s further divided into three parts: Eden space and two Survivor spaces (S0 and S1). Most objects are short-lived and die in Eden. When Eden fills up, a "Minor GC" (Garbage Collection) occurs. Objects that survive this collection are moved to one of the Survivor spaces.
Old Generation: Objects that survive multiple Minor GCs in the Young Generation are promoted to the Old Generation. These are typically longer-lived objects. The Old Generation is collected less frequently, but when it does happen, it’s a "Major GC" (or Full GC), which can be more time-consuming because it has more objects to scan.
Metaspace: This is where class metadata (like class definitions, method information, and constant pools) is stored. Before Java 8, this was called PermGen (Permanent Generation) and was part of the heap. Metaspace is stored off-heap (in native memory), which means it’s not subject to garbage collection in the same way as objects. However, it can still grow and cause issues if classes are loaded and unloaded excessively without proper management.
The garbage collector’s strategy is to make Minor GCs very fast. By allocating most objects in Eden and cleaning it frequently, it can quickly reclaim memory from short-lived objects. Only objects that have survived a few Minor GCs are moved to the Old Generation, reducing the amount of work the more expensive Major GCs have to do.
Consider the heap configuration: -Xms512m -Xmx2g -XX:NewRatio=2.
-Xms512m sets the initial heap size to 512 megabytes.
-Xmx2g sets the maximum heap size to 2 gigabytes.
-XX:NewRatio=2 means the Old Generation will be twice the size of the Young Generation (Young : Old = 1 : 2). So, if the total heap is 1.5GB (for example, after initial allocation), the Young Generation would be 500MB and the Old Generation 1GB.
The most surprising thing about garbage collection is how the collector uses the Survivor spaces. When a Minor GC happens, objects in Eden are scanned. Reachable objects are copied to one of the Survivor spaces (say, S0). If S0 is full, they might be moved to S1. The key is that only one Survivor space is used at any given time. After the collection, the Eden space is completely cleared, and the Survivor space that wasn’t used becomes the new target for surviving objects. This "copying" mechanism ensures that objects that have survived multiple collections are eventually promoted to the Old Generation.
If you’re seeing frequent Full GCs or OutOfMemoryError: Java heap space, understanding how objects flow from Young to Old is critical for tuning -XX:NewRatio, -XX:MaxNewSize, and -XX:MaxOldSize.
The next challenge you’ll encounter is tuning the specific garbage collector algorithm itself, like G1 or ZGC.