Complete companion suite for the ankurm.com guide. Everything was compiled and
executed on java 25.0.3+9-LTS-195 (AMD Ryzen 5 5600U, Windows 11); every file
under results/ is unedited program output.
Code
latency/ open-loop latency harness that measures from intended arrival, so
coordinated omission is accounted for rather than hidden. Reports
response time and service time side by side; the gap reached 2910x
on G1.
jmh/ four microbenchmarks isolating one mechanism each: TLAB allocation,
the ZGC load barrier (with a primitive-load control), the write
barrier (with null / old-to-old / primitive controls), and promotion
pressure.
tuning/ provokes ZGC allocation stalls and reads its own JFR recording back,
grouped by page class. jdk.ZAllocationStall is enabled without a
threshold, because the 10 ms default hides most stalls.
internals/ ZGC page classes vs G1 humongous, read from the live VM via
HotSpotDiagnosticMXBean and jdk.ZPageAllocation events.
analysis/ unified GC log parser that keeps stop-the-world pauses and
concurrent phases in separate buckets.
env/ environment capture; proves generational mode from JMX bean names.
Docs
Eight chapters covering the JEP 439/474/490 timeline, colored pointers and both
barriers, allocation stalls, a full flag reference, logging and JFR, benchmark
methodology, corner cases, and a decision procedure.
Headline result (60 s, 20k req/s, 2 GB heap, ~585 MB live)
p50 ZGC 0.006 ms G1 0.005 ms
p99.9 ZGC 1.437 ms G1 95.169 ms
total STW time ZGC 1.503 ms G1 1,139.624 ms
138 lines
5.3 KiB
Markdown
138 lines
5.3 KiB
Markdown
# 5. Reading the logs and the flight recording
|
|
|
|
> Runnable companion: [`analysis/.../GcLogParser.java`](../analysis/src/com/ankurm/zgc/analysis/GcLogParser.java)
|
|
|
|
## 5.1 The unified log lines you need
|
|
|
|
```
|
|
-Xlog:gc*,gc+alloc=debug:file=gc.log:time,uptime,level,tags:filecount=5,filesize=50m
|
|
```
|
|
|
|
| Selector | Gives you |
|
|
|---|---|
|
|
| `gc` | one line per collection |
|
|
| `gc+phases` | every sub-phase, including the three ZGC pauses |
|
|
| `gc+heap` | before/after occupancy |
|
|
| `gc+alloc=debug` | **`Allocation Stall` lines** — off by default, and the reason most people never see stalls |
|
|
| `gc+start`, `gc+task`, `gc+ref` | cycle triggers, worker counts, reference processing |
|
|
|
|
## 5.2 The category error that makes ZGC look terrible
|
|
|
|
A ZGC cycle produces lines like this:
|
|
|
|
```
|
|
GC(41) Minor Collection (Allocation Rate) 1180M(58%)->402M(20%) 0.051s
|
|
GC(41) y: Pause Mark Start 0.014ms
|
|
GC(41) y: Concurrent Mark 32.109ms
|
|
GC(41) y: Pause Mark End 0.011ms
|
|
GC(41) y: Concurrent Relocate 14.802ms
|
|
GC(41) y: Pause Relocate Start 0.009ms
|
|
```
|
|
|
|
A naive script that sums every duration on every line reports "51 ms of GC" for this cycle. The real
|
|
stop-the-world time is **0.034 ms** — the three `Pause` lines. The `Concurrent` phases ran alongside
|
|
the application.
|
|
|
|
G1's log has no such trap: its lines are pauses.
|
|
|
|
```
|
|
GC(12) Pause Young (Normal) (G1 Evacuation Pause) 1180M->402M(2048M) 16.784ms
|
|
```
|
|
|
|
`GcLogParser` in this repository keeps the two categories in separate buckets and prints them
|
|
separately, precisely so the comparison cannot be made accidentally. It also prints the sentence
|
|
that should accompany every such table:
|
|
|
|
```
|
|
Adding the block above to the pause block would be a category error:
|
|
concurrent time overlaps application execution by design.
|
|
```
|
|
|
|
## 5.3 The same trap exists in JMX
|
|
|
|
`GarbageCollectorMXBean.getCollectionTime()` means different things per collector:
|
|
|
|
| Bean | Reports |
|
|
|---|---|
|
|
| `ZGC Minor Cycles` / `ZGC Major Cycles` | **concurrent cycle** duration — not freeze time |
|
|
| `ZGC Minor Pauses` / `ZGC Major Pauses` | real stop-the-world time |
|
|
| `G1 Young Generation` / `G1 Old Generation` | real stop-the-world time |
|
|
| `G1 Concurrent GC` | concurrent cycle — despite sitting in the same bean list |
|
|
|
|
A dashboard that sums `getCollectionTime()` over all beans will report ZGC as spending ~1.6 seconds
|
|
"in GC" during a 60-second run where its actual freeze time rounded to **0 ms**. That is how ZGC
|
|
acquires a reputation for being slow on teams that never changed their dashboard query.
|
|
|
|
`GcObserver` in this repository classifies each bean and labels it in the output:
|
|
|
|
```
|
|
GC activity during the measured window
|
|
ZGC Minor Cycles collections=18 time= 919 ms [cycle (mostly concurrent)]
|
|
ZGC Minor Pauses collections=54 time= 0 ms [stop-the-world]
|
|
ZGC Major Cycles collections=3 time= 667 ms [cycle (mostly concurrent)]
|
|
ZGC Major Pauses collections=15 time= 0 ms [stop-the-world]
|
|
```
|
|
|
|
Note also the **3 pauses per cycle** arithmetic: 18 minor cycles → 54 minor pauses.
|
|
|
|
## 5.4 JFR events worth enabling
|
|
|
|
| Event | Default threshold | Why |
|
|
|---|---|---|
|
|
| `jdk.ZAllocationStall` | **10 ms** | The real ZGC latency source. Set to `0 ms` and enable stack traces. |
|
|
| `jdk.ZPageAllocation` | — | Shows the Small / Medium / Large split; diagnoses *which kind* of pressure. |
|
|
| `jdk.ZYoungGarbageCollection` / `jdk.ZOldGarbageCollection` | — | Cycle counts with cause. |
|
|
| `jdk.ZRelocationSet`, `jdk.ZRelocationSetGroup` | — | What is being compacted each cycle. |
|
|
| `jdk.ZUncommit`, `jdk.ZUnmap` | — | Heap being handed back to the OS. |
|
|
| `jdk.GCPhasePause` | — | Works for both collectors; the honest cross-collector pause metric. |
|
|
|
|
Starting a recording programmatically, which is what both demos in this repository do:
|
|
|
|
```java
|
|
try (Recording recording = new Recording()) {
|
|
recording.enable("jdk.ZAllocationStall").withoutThreshold().withStackTrace();
|
|
recording.setToDisk(true);
|
|
recording.setDestination(Path.of("stalls.jfr"));
|
|
recording.start();
|
|
// ... workload ...
|
|
recording.stop();
|
|
}
|
|
```
|
|
|
|
Reading it back without any external tool:
|
|
|
|
```java
|
|
try (RecordingFile file = new RecordingFile(Path.of("stalls.jfr"))) {
|
|
while (file.hasMoreEvents()) {
|
|
RecordedEvent e = file.readEvent();
|
|
if (e.getEventType().getName().equals("jdk.ZAllocationStall")) { ... }
|
|
}
|
|
}
|
|
```
|
|
|
|
> **A `RecordedEvent` gotcha that costs an hour.** `getValue` is declared `<T> T getValue(String)`.
|
|
> Writing `String.valueOf(e.getValue("type"))` lets javac infer `T = char[]`, because
|
|
> `String.valueOf(char[])` is an applicable overload — and you get
|
|
> `ClassCastException: class java.lang.String cannot be cast to class [C` at runtime. Bind to an
|
|
> `Object` local first. Similarly `RecordedEvent::getDuration` is overloaded, so a method reference
|
|
> to it will not compile inside a `Comparator.comparing(...)`; use an explicit lambda.
|
|
|
|
And from the command line, with the stack that was stalled:
|
|
|
|
```console
|
|
$ jfr summary stalls.jfr
|
|
$ jfr print --events ZAllocationStall --stack-depth 10 stalls.jfr
|
|
jdk.ZAllocationStall {
|
|
startTime = 22:44:24.809 (2026-07-30)
|
|
duration = 2.20 ms
|
|
type = "Small"
|
|
size = 2.0 MB
|
|
eventThread = "allocator-3" (javaThreadId = 43)
|
|
stackTrace = [ ... ]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
Next: [06-g1-in-jdk25.md](06-g1-in-jdk25.md).
|