Back to CI/CD Pipeline
Q9Question 9 of 56

Why ZGC can still show a huge periodic latency spike: the allocation stall: how ZUncommit worsens bursty workloads, and how to fix it

Key Concepts
  • An allocation stall happens when the application allocates faster than ZGC can concurrently reclaim, so threads are parked waiting for free memory.
  • ZGC's sub-millisecond guarantee is about GC pauses, not about application waits from running out of memory.
  • ZUncommit returns unused heap to the OS during quiet periods, but re-committing pages during a burst causes page faults and zeroing.
  • Fix 1: set -Xms equal to -Xmx to disable uncommit and keep the heap fixed.
  • Fix 2: add AlwaysPreTouch to fault in all pages at startup so bursts never pay page-fault costs.
  • Fix 3: use Generational ZGC (default from Java 23) so the young generation is collected frequently and free space stays ahead of allocation.

Definition

ZGC's pauses stay tiny, but if allocation outruns reclamation the app stalls waiting for memory, and ZUncommit makes bursts worse by forcing re-commit; fix it with fixed heap, pre-touch, and generational mode.
  • Allocation stall – the application's allocation rate outpaces ZGC's concurrent reclamation, so the allocating thread is parked until memory is freed; this is NOT a GC pause.
  • ZUncommit – a feature where ZGC returns unused heap pages to the OS during quiet periods to reduce resident footprint.
  • Page faults + zeroing – the cost of re-acquiring committed pages from the OS when a burst arrives after ZUncommit; adds latency at the worst moment.
  • AlwaysPreTouch – a JVM flag that faults in and zeroes every heap page at startup, paying the cost once instead of during live bursts.
  • Generational ZGC – collects the young generation frequently and cheaply, keeping free space ahead of the allocation rate.

The renovation crew (ZGC) normally works in the background without stopping customers. But if a sudden rush fills every desk faster than the crew can clear old ones, a new customer has nowhere to sit and must wait at the door: that is an allocation stall. Worse, if during a quiet hour the bank returned some desks to the landlord (ZUncommit), then when the rush hits it must first get those desks back and clean them (page faults) before anyone can sit.

Understanding the Concepts

What is an allocation stall?

ZGC's famous sub-millisecond pause refers to the stop-the-world portion of a GC cycle. It does not promise the application never waits. If the application allocates faster than ZGC can reclaim, free memory runs out and the allocating thread is parked.

BASH
# GC log showing an allocation stall
java -Xlog:gc*:file=zgc.log -XX:+UseZGC -Xmx8g -jar app.jar

# Look for:
# [12.345s] Allocation Stall (main) 234.567ms
TEXT
Allocation stall timeline:
  Time 0: Application allocating at 2 GB/sec
  Time 1: ZGC concurrently reclaiming at 1.5 GB/sec
  Time 2: Free memory runs out!
  Time 3: Thread parked (ALLOCATION STALL) until ZGC frees space
  Time 4: ZGC frees pages, thread unparked

  The stall appears as a huge latency spike in metrics

What causes allocation stalls?

Three common causes: the heap is too small for the allocation rate, the allocation rate is spiky, or non-generational mode wastes effort scanning the whole heap.

JAVA
package com.bank.service;

import java.util.ArrayList;
import java.util.List;

public class SettlementBatch {

    // High allocation rate during end-of-day settlement
    public void processSettlement() {
        List<byte[]> batchData = new ArrayList<>();
        for (int i = 0; i < 100_000; i++) {
            // 100 KB per trade = 10 GB total burst
            batchData.add(new byte[100 * 1024]);
        }
        // Process and discard
        settle(batchData);
    }

    private void settle(List<byte[]> data) { }
}

How does ZUncommit make bursts worse?

ZUncommit returns heap memory to the OS during quiet periods. When the next burst arrives, the JVM must re-commit those pages, causing page faults and CPU-costly zeroing.

TEXT
ZUncommit timeline:
  Quiet period: ZGC returns 4 GB of pages to OS
  Resident set: drops from 8 GB to 4 GB (nice for monitoring)

  Burst arrives:
  Application needs 8 GB again
  OS must re-map and zero 4 GB of pages
  Page faults + zeroing: 100-500ms latency spike!
BASH
# Check if ZUncommit is active
java -XX:+UseZGC -Xmx8g -Xlog:gc+heap=debug -jar app.jar
# Look for: "Uncommit" entries in gc.log

How do you fix allocation stalls?

Three fixes work together: fixed heap to disable uncommit, pre-touch to fault pages at startup, and generational mode for efficient young collection.

BASH
# Fix 1: Fixed heap (disable uncommit)
java -XX:+UseZGC \
     -Xms16g -Xmx16g \   # min == max → heap never shrinks
     -jar settlement-service.jar

# Fix 2: Pre-touch all pages at startup
java -XX:+UseZGC \
     -Xms16g -Xmx16g \
     -XX:+AlwaysPreTouch \  # fault + zero all pages at startup
     -jar settlement-service.jar

# Fix 3: Generational mode (default from Java 23)
java -XX:+UseZGC \
     -Xms16g -Xmx16g \
     -XX:+AlwaysPreTouch \
     -jar settlement-service.jar
     # Java 23+: generational by default

# For Java 21-22, add -XX:+ZGenerational explicitly
java -XX:+UseZGC -XX:+ZGenerational \
     -Xms16g -Xmx16g -XX:+AlwaysPreTouch \
     -jar settlement-service.jar

How do the three fixes work together?

| Fix | What it does | What it prevents |
|---|---|---|
| -Xms == -Xmx | Fixed heap, no uncommit | Re-commit page faults during bursts |
| AlwaysPreTouch | Fault + zero all pages at startup | First-use page faults during live traffic |
| ZGenerational | Frequent young collections | Stalls from slow whole-heap scanning |

How do you detect allocation stalls in production?

Monitor the GC log for allocation stall events and correlate with traffic patterns.

BASH
# Count allocation stalls
grep -c "Allocation Stall" zgc.log

# Extract stall durations
grep "Allocation Stall" zgc.log \
  | awk '{print $NF}' | sort -n
TEXT
# Before fix:
Allocation Stall count: 12
Durations: 45ms, 89ms, 123ms, 234ms, 567ms

# After fix:
Allocation Stall count: 0

What are common mistakes?

The most common mistake is assuming ZGC eliminates all latency spikes. ZGC eliminates GC pauses; it does not eliminate the consequences of running out of memory.

BASH
# WRONG: assume ZGC means no tuning needed
java -XX:+UseZGC -Xmx4g -jar high-alloc-rate-service.jar
# Result: allocation stalls under load

# CORRECT: give ZGC headroom and apply all three fixes
java -XX:+UseZGC -Xms8g -Xmx8g -XX:+AlwaysPreTouch \
     -jar high-alloc-rate-service.jar

What should a senior engineer know?

A senior engineer proves the fix by alerting on the allocation-stall count in the GC log against the trade-volume curve. At National Bank of India, the end-of-day settlement service stalled for hundreds of milliseconds at market close. Fixed heap, pre-touch, and generational mode eliminated the stalls completely.

Visual Overview

DIAGRAM
  ZGC allocation stall:
  ──────────────────────────────────────────────
  Application:  alloc ──→ alloc ──→ alloc ──→ NO FREE MEMORY!
                                                    │
  ZGC (concurrent): reclaim ──→ reclaim ──→ reclaim │
                     (too slow to keep up)           │
                                                    ▼
                                            Thread PARKED
                                            (allocation stall)
                                                    │
  ZGC frees pages ──────────────────────────────────┘
  Thread UNPARKED, allocation resumes

  Fix:
  ┌───────────────────────────────────────────────┐
  │ -Xms == -Xmx       → no uncommit             │
  │ +AlwaysPreTouch     → no page faults          │
  │ +ZGenerational      → fast young collections  │
  │ Enough headroom     → ZGC stays ahead          │
  └───────────────────────────────────────────────┘

Key Takeaway

ZGC's sub-millisecond guarantee covers the stop-the-world portion of a collection. It does not promise the application never waits. If allocation outruns reclamation, free memory runs out and the allocating thread is parked: that is an allocation stall, and it shows up as a large latency spike under load.

ZUncommit compounds the problem by returning heap pages to the OS during quiet periods. When the next burst arrives, re-committing those pages costs page faults and zeroing at the worst moment. The fix is three things working together: fixed heap so no pages are ever returned, AlwaysPreTouch so every page is faulted in at startup, and Generational ZGC so the young generation is collected frequently and free space stays ahead.

At National Bank of India, the settlement service stalled at market close until these three fixes eliminated every allocation stall event in the GC log.

Remember one sentence: ZGC eliminates GC pauses but not allocation stalls, so fix the heap, pre-touch the pages, and run generationally to keep free memory ahead of the allocation rate.

---

Share
Use ← → arrow keys to navigate