Java JVM Garbage Collection Algorithms and Memory Model Explained
The Java Virtual Machine automates memory management through a partitioned runtime data model—comprising thread-private stacks and a shared heap—while employing garbage collection algorithms like mark-compact, copying, and region-based concurrent marking to reclaim unreachable objects with minimal application pause times.
The Snailclimb/JavaGuide repository provides authoritative documentation on how the JVM organizes memory and reclaims it automatically. Mastering the Java JVM garbage collection algorithms and memory model is essential for diagnosing OutOfMemoryError exceptions, eliminating memory leaks, and tuning application latency.
JVM Memory Model and Runtime Data Areas
The JVM specification defines distinct logical regions for storing data during program execution. According to the repository's detailed breakdown in docs/java/jvm/memory-area.md, these areas are categorized by thread isolation and lifecycle.
Thread-Private Areas
Each Java thread maintains its own isolated storage to prevent interference during concurrent execution.
- Program Counter (PC) Register: Stores the current byte-code offset of the method being executed. This region is extremely small and never throws
OutOfMemoryError. - Java Stack: Contains stack frames for each method invocation, holding local variables, operand stacks, and dynamic linking information. It throws
StackOverflowErrorwhen recursion exceeds capacity orOutOfMemoryErrorif expansion fails. - Native Method Stack: Dedicated to frames for native (C/C++) methods invoked via JNI. It follows the same overflow rules as the Java Stack.
Shared Memory Areas
All threads access these regions concurrently, requiring synchronization mechanisms and garbage collection oversight.
- Heap: The largest runtime data area, storing all object instances and arrays. It is the primary focus of Java JVM garbage collection algorithms and is subject to
OutOfMemoryErrorwhen allocation requests cannot be satisfied. - Method Area / Metaspace: Stores class metadata including the runtime constant pool, field and method data, and bytecode. Since JDK 8, the HotSpot JVM implements this as native Metaspace (replacing the permanent generation), which can trigger
OutOfMemoryError: Metaspacewhen class metadata exhausts available native memory. - Direct (Off-Heap) Memory: Allocated via
java.nioor JNI, this region operates outside the GC-managed heap. Developers must explicitly release these buffers or rely on finalization to avoid exhausting OS memory.
Heap Generations
The heap is further partitioned to optimize object allocation and collection efficiency based on generational hypothesis.
- Young Generation: Contains the Eden space and two Survivor spaces (S0 and S1). Most objects are allocated here and die quickly. Minor GC events clean this region using copying algorithms.
- Old Generation: Holds objects that have survived sufficient Minor GC cycles (tenuring). These long-lived objects are collected less frequently using mark-compact or mark-sweep algorithms.
- Metaspace: While logically separate from the heap, this region holds class metadata and is managed independently, though class unloading during Full GC can reclaim space here.
Garbage Collection Algorithms
Garbage collectors identify and reclaim unreachable objects using specific traversal and compaction strategies documented in docs/java/jvm/jvm-garbage-collection.md.
Mark-Sweep
The collector traverses the object graph from GC roots (local variables, static fields, JNI references), marks live objects, then sweeps the heap to reclaim unmarked memory. While simple, this algorithm creates memory fragmentation. It serves as the foundation for the CMS collector and the Full GC phase of Serial/Parallel collectors.
Copying (Semispace)
This approach divides memory into from and to spaces. Live objects are copied from the source to the destination, leaving the entire source region free and contiguous. The Serial, Parallel Scavenge, and G1 (for young generation) collectors utilize this algorithm for its speed and natural defragmentation, though it requires 50% memory overhead.
Mark-Compact
After marking live objects, the collector moves (compacts) them toward one end of the heap, eliminating fragmentation entirely. This algorithm is employed by Serial Old, Parallel Old, and G1 (for old generation) collectors, ensuring contiguous free memory for large object allocations.
Region-Based Concurrent Algorithms
Modern low-latency collectors split the heap into equal-sized regions. ZGC (JDK 11+) and Shenandoah perform marking and relocation concurrently with application threads using read/write barriers. These algorithms enable sub-millisecond pause times regardless of heap size by processing regions incrementally.
JVM Garbage Collectors Implementation
The HotSpot JVM provides multiple collector implementations, selectable via command-line flags, each combining the algorithms above for specific generational regions.
Serial and Parallel Collectors
- Serial GC: Uses a single thread for both young generation (copying) and old generation (mark-compact) collection. Activate with
-XX:+UseSerialGCfor small footprint applications. - Parallel (Throughput) GC: Employs multiple threads for parallel scavenging and compacting, maximizing application throughput. Enable via
-XX:+UseParallelGC.
CMS (Concurrent Mark-Sweep)
Designed to minimize pause times for the old generation, CMS performs most marking and sweeping concurrently with application threads. While it reduces stop-the-world pauses, it can suffer from fragmentation and requires a Full GC fallback. It is activated with -XX:+UseConcMarkSweepGC (deprecated in JDK 9, removed in JDK 14).
G1 (Garbage-First)
G1 partitions the heap into regions and prioritizes collecting regions with the most garbage. It uses copying for young generation evacuation and mark-compact for old generation mixed collections. Target pause times are configurable via -XX:MaxGCPauseMillis. Enable with -XX:+UseG1GC, recommended for heap sizes larger than 6GB.
ZGC and Shenandoah
- ZGC: A scalable, low-latency collector that handles all generations using concurrent region-based marking and compaction with load barriers. It maintains pause times below 10ms even with terabyte-sized heaps. Use
-XX:+UseZGCon JDK 11+. - Shenandoah: An OpenJDK collector featuring concurrent evacuation of objects, reducing pause times independent of heap size. Activate with
-XX:+UseShenandoahGC.
Practical Code Examples
Triggering Minor GC and Inspecting Eden
Run this example with -XX:+PrintGCDetails -Xmx256m to observe Minor GC events in the Young Generation.
public class MinorGC {
public static void main(String[] args) throws InterruptedException {
// Allocate many small objects to fill Eden
for (int i = 0; i < 1_000_000; i++) {
byte[] b = new byte[1024]; // 1 KB each
}
// Suggest a GC (not guaranteed)
System.gc();
Thread.sleep(1000); // Give GC time to log
}
}
Demonstrating Object Promotion (Tenuring)
This example forces early promotion to the Old Generation by adjusting the tenuring threshold.
public class TenuringDemo {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
allocate(); // each call creates many short-lived objects
System.gc(); // force a Minor GC after each batch
}
}
private static void allocate() {
// Objects survive several GCs → promoted to Old Gen
for (int i = 0; i < 100_000; i++) {
byte[] b = new byte[1024]; // 1 KB
}
}
}
Execute with -XX:MaxTenuringThreshold=1 to see objects promoted to Old Gen after surviving only one collection.
Using ZGC for Low-Latency Allocation
This example demonstrates ZGC's behavior under heavy allocation pressure.
public class ZGCDemo {
public static void main(String[] args) throws InterruptedException {
while (true) {
// Allocate large objects continuously
byte[] b = new byte[10 * 1024 * 1024]; // 10 MB
Thread.sleep(10);
}
}
}
Run with:
java -XX:+UseZGC -Xmx2g ZGCDemo
ZGC maintains pause times typically below 10 milliseconds even as allocation rates increase.
Tuning and Selection Guidelines
Selecting the appropriate collector depends on application requirements:
- Throughput-critical batch jobs: Use Parallel GC (
-XX:+UseParallelGC) to maximize raw processing speed. - Low-latency services: Deploy G1 (
-XX:+UseG1GC) for JDK 8+ or ZGC (-XX:+UseZGC) for JDK 11+ to maintain sub-100ms pause times. - Large heap sizes (>100GB): ZGC or Shenandoah provide consistent latency regardless of heap scale.
Key tuning parameters include:
-XX:NewSizeand-XX:MaxNewSize: Control Young Generation boundaries.-XX:SurvivorRatio: Adjusts Eden versus Survivor space sizing.-XX:MaxTenuringThreshold: Defines how many GC cycles objects survive before promotion.-XX:InitiatingHeapOccupancyPercent: Sets the heap occupancy percentage that triggers concurrent marking in G1.
Summary
- The JVM memory model divides runtime data into thread-private stacks/registers and shared heap/Metaspace regions, as detailed in
docs/java/jvm/memory-area.md. - The heap is generational, separating short-lived objects in the Young Generation (Eden + Survivors) from long-lived objects in the Old Generation.
- Garbage collection algorithms include Mark-Sweep (fragmenting), Copying (fast but 50% overhead), and Mark-Compact (defragmenting).
- Modern collectors combine these algorithms: G1 uses region-based copying and compaction, while ZGC employs concurrent region-based marking with load barriers for sub-10ms pauses.
- Selection and tuning via flags like
-XX:+UseZGCand-XX:MaxGCPauseMillisallow optimization for specific latency and throughput requirements.
Frequently Asked Questions
What is the difference between Young Generation and Old Generation in the JVM?
The Young Generation (Eden space plus two Survivor spaces) holds newly allocated objects and is collected frequently via fast copying algorithms. The Old Generation stores objects that have survived multiple Young Generation collections (tenuring) and is collected less frequently using mark-sweep or mark-compact algorithms. Most objects die young, so separating generations optimizes collection efficiency.
How does ZGC achieve ultra-low pause times?
ZGC divides the heap into regions and performs marking, relocation, and remapping concurrently with application threads using colored pointers and load barriers. It avoids stopping the world for heap compaction by relocating objects incrementally and updating references on access, keeping pause times independent of heap size (typically under 10ms).
What causes OutOfMemoryError in Metaspace?
OutOfMemoryError: Metaspace occurs when the JVM exhausts native memory allocated for class metadata. This typically happens during excessive dynamic class generation (e.g., by reflection proxies, CGLIB, or scripting engines) without unloading class loaders. Unlike the old PermGen, Metaspace uses native memory and grows until the OS limit or -XX:MaxMetaspaceSize is reached.
When should I use G1 GC instead of Parallel GC?
Use G1 GC (-XX:+UseG1GC) when your application requires predictable pause times (targetable via -XX:MaxGCPauseMillis) or runs with large heaps (6GB+), as it collects regions incrementally. Use Parallel GC (-XX:+UseParallelGC) for batch processing or scientific computing where raw throughput matters more than occasional long pauses, and heap sizes are moderate.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →