# Java JVM Garbage Collection Algorithms and Memory Model Explained

> Understand Java JVM garbage collection algorithms like mark-compact and copying, and grasp the memory model for efficient application performance.

- Repository: [Guide/JavaGuide](https://github.com/Snailclimb/JavaGuide)
- Tags: deep-dive
- Published: 2026-02-24

---

**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](https://github.com/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`](https://github.com/Snailclimb/JavaGuide/blob/main/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 `StackOverflowError` when recursion exceeds capacity or `OutOfMemoryError` if 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 `OutOfMemoryError` when 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: Metaspace` when class metadata exhausts available native memory.
- **Direct (Off-Heap) Memory**: Allocated via `java.nio` or 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`](https://github.com/Snailclimb/JavaGuide/blob/main/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:+UseSerialGC` for 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:+UseZGC` on 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.

```java
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.

```java
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.

```java
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:

```bash
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:NewSize` and `-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`](https://github.com/Snailclimb/JavaGuide/blob/main/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:+UseZGC` and `-XX:MaxGCPauseMillis` allow 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.