# JEP 404: Generational Shenandoah

* <https://wiki.openjdk.org/display/shenandoah/Main> - theory
* <https://www.azul.com/downloads/?package=jdk> - azul jdk support Shenandoah
* <https://builds.shipilev.net/openjdk-jdk/> - if you are brave
* education
  * &#x20;[Getting Started with ZGC](https://www.youtube.com/watch?v=wpkbJGRCwRo\&ab_channel=Java)
  * <https://wiki.openjdk.org/display/shenandoah/Main>

Why :

* Classic Shenandoah = low-pause, concurrent GC
* Generational GC = separates **young** and **old** objects
* GOAL - low GC Pause

#### &#x20;

| Feature                 | **Non-Generational Shenandoah** (pre-Java 24)                 | **Generational Shenandoah** (JEP 404, Java 24)                        |
| ----------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------- |
| 🧠 GC Model             | Single-heap, no distinction between object ages               | Divides heap into **Young Gen** and **Old Gen**                       |
| 🔄 GC Efficiency        | Less efficient for workloads with many short-lived objects    | Much more efficient for real-world workloads (most objects die young) |
| 🚮 Collection Frequency | All objects treated equally → full-heap collection more often | **Young Gen collected more frequently**, Old Gen collected rarely     |
| ⏱ Pause Time            | Still low, but not optimal for large heaps                    | Lower pause times and better throughput                               |
| ♻️ Promotion            | N/A — everything in one space                                 | Surviving young-gen objects get promoted to old-gen                   |
| 📈 Throughput           | Lower in long-running, object-heavy apps                      | Improved performance in those apps                                    |
| 🧰 Suitable For         | Simpler GC tuning, small heaps, latency-sensitive             | Production-grade, high-throughput, scalable apps                      |

### Step 1 - Enable Generational Shenandoah

```
java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseShenandoahGC \
  -XX:ShenandoahGCMode=generational \
  -Xlog:gc \
  -version
```

### Step 2 - Run Demo class

```
java \                                                                                                                                                                                          -XX:+UnlockExperimentalVMOptions \
  -XX:+UseShenandoahGC \
  -XX:ShenandoahGCMode=generational \
  -Xlog:gc \
  -Xmx512m \
  -cp target/classes \
  com.wlodar.jeeps.jep404Shenandoah.GenerationalShenandoahDemo
```

logs for xmx512:&#x20;

```
Performing Allocation
[0.120s][info][gc] Trigger (Young): Learning 1 of 5. Free (357M) is below initial threshold (358M)
[0.120s][info][gc] GC(0) Concurrent reset (Young) 0.156ms
[0.121s][info][gc] GC(0) Pause Init Mark (Young) 0.040ms
[0.121s][info][gc] GC(0) Concurrent remembered set scanning 0.088ms
[0.121s][info][gc] GC(0) Concurrent marking roots 0.233ms
[0.123s][info][gc] GC(0) Concurrent marking (Young) 1.436ms
[0.123s][info][gc] GC(0) Pause Final Mark (Young) 0.224ms
[0.123s][info][gc] GC(0) Concurrent weak references (Young) 0.048ms
[0.124s][info][gc] GC(0) Concurrent weak roots (Young) 0.415ms
[0.124s][info][gc] GC(0) Concurrent cleanup (Young) 135M->109M(492M) 0.045ms
[0.124s][info][gc] GC(0) Pause Final Roots (Young) 0.024ms
[0.135s][info][gc] Trigger (Young): Learning 2 of 5. Free (357M) is below initial threshold (358M)
[0.137s][info][gc] GC(1) Concurrent reset (Young) 1.764ms
[0.138s][info][gc] GC(1) Pause Init Mark (Young) 0.039ms
[0.138s][info][gc] GC(1) Concurrent remembered set scanning 0.045ms
[0.138s][info][gc] GC(1) Concurrent marking roots 0.140ms
[0.139s][info][gc] GC(1) Concurrent marking (Young) 1.121ms
[0.140s][info][gc] GC(1) Pause Final Mark (Young) 0.168ms
[0.140s][info][gc] GC(1) Concurrent weak references (Young) 0.015ms
```

logs for xmx128:\\

```
[0.058s][info][gc] Trigger (Young): Learning 1 of 5. Free (19968K) is below initial threshold (91750K)
[0.058s][info][gc] GC(0) Concurrent reset (Young) 0.098ms
[0.102s][info][gc] GC(0) Pause Init Mark (Young) 0.045ms
Performing Allocation
[0.102s][info][gc] GC(0) Concurrent remembered set scanning 0.094ms
[0.102s][info][gc] GC(0) Concurrent marking roots 0.259ms
[0.104s][info][gc] GC(0) Concurrent marking (Young) 1.708ms
[0.105s][info][gc] GC(0) Pause Final Mark (Young) 0.158ms
[0.105s][info][gc] GC(0) Concurrent thread roots 0.063ms
[0.105s][info][gc] GC(0) Concurrent weak references (Young) 0.044ms
[0.105s][info][gc] GC(0) Concurrent weak roots (Young) 0.384ms
[0.105s][info][gc] GC(0) Concurrent cleanup (Young) 106M->106M(128M) 0.007ms
[0.106s][info][gc] GC(0) Concurrent strong roots 0.436ms
[0.107s][info][gc] GC(0) Concurrent evacuation 0.874ms
[0.107s][info][gc] GC(0) Pause Init Update Refs 0.013ms
[0.108s][info][gc] GC(0) Concurrent update references 0.882ms
[0.109s][info][gc] GC(0) Concurrent update thread roots 0.565ms
[0.109s][info][gc] GC(0) Pause Final Update Refs 0.037ms
[0.109s][info][gc] GC(0) Concurrent cleanup (Young) 113M->111M(128M) 0.017ms

```

These logs show **Generational ZGC** in action, handling memory pressure with frequent **young generation collections**. Here’s what’s happening:

* `Trigger (Young): Learning ...` indicates that ZGC is in its **learning phase**, adjusting internal heuristics and thresholds based on observed behavior. The GC is triggered because **free memory dropped below a threshold** (e.g., `357M < 358M`).
* `GC(0) Concurrent reset (Young)` begins the collection by resetting internal GC state. This phase runs **concurrently**, meaning the application keeps running.
* `Pause Init Mark (Young)` is a **short stop-the-world phase** to mark GC roots.
* The following stages (`remembered set scanning`, `marking roots`, `marking`, `weak references`, etc.) all run **concurrently**, making ZGC very low-pause.
* `Pause Final Mark (Young)` is another brief pause to finish marking before cleanup.
* `Concurrent cleanup (Young) 135M->109M(492M)` shows how much memory was reclaimed (from 135MB down to 109MB out of 492MB).
* `Pause Final Roots (Young)` is the final short pause before GC finishes.

📌 The key idea: **Generational ZGC** maintains very short pause times by doing most of its work concurrently — including root scanning and cleanup — while focusing primarily on the **young generation**, just like Gen Shenandoah or G1.

**OBSERVATION :**&#x20;

```
[0.109s][info][gc] GC(0) Concurrent cleanup (Young) 113M->111M(128M) 0.017ms
```

100MB allocated as "long lived" are promoted

The `longLived` object is allocated in the Young Generation, but since it remains referenced across many GC cycles, it is eventually **promoted to the Old Generation** by Generational ZGC. While ZGC logs don’t explicitly label promotions, you can infer them when memory usage in the Young Gen stays high between collections. For deeper analysis, use **JFR** or enable verbose diagnostics.

### Comparison with G1:

```
chmod +x run-gc.sh

mvn compile

./scripts/jep404/run-gc.sh           # Defaults to Generational Shenandoah
./scripts/jep404/run-gc.sh gen       # Explicit Generational Shenandoah
./scripts/jep404/run-gc.sh g1        # G1 Garbage Collector
./scripts/jep404/run-gc.sh zgc       # Z Garbage Collector


```

optionally compare logs

```
./scripts/jep404/run-gc.sh gen > gc-gen.log
./scripts/jep404/run-gc.sh g1  > gc-g1.log
./scripts/jep404/run-gc.sh zgc > gc-zgc.log
diff gc-gen.log gc-g1.log

```

logs for G1 :

```
Performing Allocation
[0.129s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 222M->102M(496M) 3.301ms
[0.129s][info][gc] GC(1) Concurrent Undo Cycle
[0.129s][info][gc] GC(1) Concurrent Undo Cycle 0.298ms
[0.140s][info][gc] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) 221M->102M(496M) 1.287ms
[0.140s][info][gc] GC(3) Concurrent Undo Cycle
[0.140s][info][gc] GC(3) Concurrent Undo Cycle 0.191ms
[0.151s][info][gc] GC(4) Pause Young (Concurrent Start) (G1 Humongous Allocation) 221M->102M(496M) 1.401ms
[0.151s][info][gc] GC(5) Concurrent Undo Cycle
[0.151s][info][gc] GC(5) Concurrent Undo Cycle 0.217ms
[0.162s][info][gc] GC(6) Pause Young (Concurrent Start) (G1 Humongous Allocation) 221M->102M(496M) 1.523ms
[0.162s][info][gc] GC(7) Concurrent Undo Cycle
[0.162s][info][gc] GC(7) Concurrent Undo Cycle 0.155ms
[0.174s][info][gc] GC(8) Pause Young (Concurrent Start) (G1 Humongous Allocation) 221M->102M(496M) 1.313ms

```

The logs show how **G1 (Garbage-First) GC** handles memory under pressure from frequent large allocations (in this case, "humongous" objects):

* `Pause Young (Concurrent Start)` means G1 is collecting the **young generation** and starting a concurrent phase afterward.
* `(G1 Humongous Allocation)` indicates the allocation triggered a collection because the object was **too large to fit in a regular region** (G1 treats any object ≥ 50% of region size as *humongous*).
* `222M->102M(496M)` shows heap usage before → after GC, with total heap capacity.
* `Concurrent Undo Cycle` is G1’s mechanism to **roll back partially completed concurrent phases**, likely because another GC had to interrupt or overlap.

The logs reflect a pattern of rapid **Young GCs** due to aggressive allocation, which is typical for G1 when large short-lived objects are created in tight loops.

### Standard non-generational ZGC

```
[0.230s][info][gc] GC(0) Major Collection (Warmup)
[0.242s][info][gc] GC(0) Major Collection (Warmup) 106M(21%)->108M(21%) 0.012s
[0.250s][info][gc] GC(1) Major Collection (Warmup)
[0.259s][info][gc] GC(1) Major Collection (Warmup) 108M(21%)->108M(21%) 0.009s
Performing Allocation
[0.280s][info][gc] GC(2) Major Collection (Warmup)
[0.299s][info][gc] GC(2) Major Collection (Warmup) 158M(31%)->206M(40%) 0.019s
[0.340s][info][gc] GC(3) Minor Collection (Allocation Rate)
[0.344s][info][gc] GC(3) Minor Collection (Allocation Rate) 334M(65%)->142M(28%) 0.004s
[0.367s][info][gc] GC(4) Major Collection (Allocation Rate)
[0.385s][info][gc] GC(4) Major Collection (Allocation Rate) 350M(68%)->252M(49%) 0.018s
[0.392s][info][gc] GC(5) Minor Collection (Allocation Rate)
[0.396s][info][gc] GC(5) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s
[0.416s][info][gc] GC(6) Minor Collection (Allocation Rate)
[0.420s][info][gc] GC(6) Minor Collection (Allocation Rate) 380M(74%)->140M(27%) 0.003s
[0.437s][info][gc] GC(7) Minor Collection (Allocation Rate)
[0.441s][info][gc] GC(7) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s
[0.451s][info][gc] GC(8) Minor Collection (Allocation Rate)
[0.456s][info][gc] GC(8) Minor Collection (Allocation Rate) 300M(59%)->172M(34%) 0.004s
[0.469s][info][gc] GC(9) Minor Collection (Allocation Rate)
[0.472s][info][gc] GC(9) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s
[0.485s][info][gc] GC(10) Minor Collection (Allocation Rate)
[0.489s][info][gc] GC(10) Minor Collection (Allocation Rate) 332M(65%)->172M(34%) 0.004s

```

**non-generational ZGC** treats the entire heap as a single space (no young/old gen separation)

#### Key GC Events in the Log:

**🔸 Warmup GCs:**

```
scssCopyEdit[0.230s][info][gc] GC(0) Major Collection (Warmup)
[0.242s][info][gc] GC(0) Major Collection (Warmup) 106M(21%)->108M(21%) 0.012s
```

* ZGC warms up its GC cycles to calibrate thresholds and prepare for regular allocations.
* These are full-heap collections (because ZGC has no generational layout yet).
* Minor memory changes here indicate low allocation pressure during warmup.

**🔸 Major Collection (Allocation Rate):**

```
scssCopyEdit[0.367s][info][gc] GC(4) Major Collection (Allocation Rate)
[0.385s][info][gc] GC(4) Major Collection (Allocation Rate) 350M->252M 0.018s
```

* A **major collection** reclaims memory across the whole heap.
* Triggered when allocation rate increases and ZGC decides a full collection is needed.

**🔸 Minor Collections:**

```
scssCopyEdit[0.340s][info][gc] GC(3) Minor Collection (Allocation Rate)
[0.344s][info][gc] GC(3) Minor Collection (Allocation Rate) 334M->142M 0.004s
```

* Minor collections still happen in **non-generational ZGC** but aren’t targeting a young gen (yet).
* These are shorter and more frequent when short-lived objects dominate.

Why non-generational ZGC is faster for simple example?\\

TL;DR;

GenerationZGC prevents : [Allocation Stalls](https://gceasy.io/gc-recommendations/allocation-stall-solution.jsp)

**Standard ZGC** shows very short GC pause times in this example (e.g., `0.004s`) because it avoids the overhead of generational tracking.\
While **Generational ZGC** introduces a Young/Old generation split (like G1 or Shenandoah), it adds bookkeeping costs that make its pause times **slightly longer** in synthetic allocation-heavy workloads.\
In real-world apps with longer-lived objects, **Generational ZGC scales better** by avoiding full-heap collection and promoting surviving objects.

Longer:

ven though **Generational ZGC** was added in Java 21+ (and improved in 22–24), **non-generational ZGC** can sometimes **show lower pause times** due to:

#### 1. 🧠 **Simpler collection process**

* In non-generational ZGC, there’s no remembered set or generational promotion tracking.
* Less internal bookkeeping = faster processing per GC cycle.

#### 2. ⚖️ **Smaller per-cycle workload**

* Each collection may reclaim fewer objects but also does less work.
* Minor GC in non-gen ZGC is *less accurate but cheaper* than Gen ZGC's more structured young collection.

#### 3. 🚀 **Concurrent design**

* ZGC is designed to minimize pause times (target: <1ms).
* Most of the GC work (mark, relocate, remap) is **fully concurrent** in both modes.
* Differences in **implementation overhead** can lead to *non-generational mode being slightly faster in synthetic tests*.

#### 4. 🧪 **Your test is allocation-heavy but not aging objects**

* In your test, objects don’t survive long enough to promote.
* So Generational ZGC’s "promotion + remembered set" logic doesn’t get much benefit, but **still incurs its cost**.
