Generational ZGC vs G1 on JDK 25: benchmarks, internals and captured results
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
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# build output
|
||||||
|
out/
|
||||||
|
target/
|
||||||
|
*.class
|
||||||
|
jmh/dependency-reduced-pom.xml
|
||||||
|
|
||||||
|
# JFR recordings produced by the demos (they are regenerated on every run)
|
||||||
|
*.jfr
|
||||||
|
|
||||||
|
# raw unified GC logs -- large, machine-specific, regenerated by scripts/run-all
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# local run bookkeeping
|
||||||
|
run-all-transcript.txt
|
||||||
|
run-all-errors.txt
|
||||||
|
|
||||||
|
# the blog post draft lives on ankurm.com, not here
|
||||||
|
BLOGPOST.html
|
||||||
|
|
||||||
|
# editors
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# results/ IS committed on purpose -- it is the evidence for the blog post
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Ankur Mhatre
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
137
README.md
Normal file
137
README.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Generational ZGC vs G1 on JDK 25 — runnable benchmarks and internals
|
||||||
|
|
||||||
|
Companion repository for **[Generational ZGC on JDK 25: Benchmarks vs G1](https://ankurm.com/generational-zgc-jdk-25-vs-g1/)**
|
||||||
|
on [ankurm.com](https://ankurm.com).
|
||||||
|
|
||||||
|
The blog post covers the concepts most people need. **This repository covers everything** — every
|
||||||
|
flag, every failure mode, every measurement pitfall, with runnable code, real captured output, and
|
||||||
|
the reasoning behind each choice.
|
||||||
|
|
||||||
|
Everything here was compiled and executed on `java 25.0.3+9-LTS-195`. Raw, unedited output for every
|
||||||
|
run is in [`results/`](results).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```console
|
||||||
|
git clone https://ankurm.com/git.app/asmhatre/zgc-jdk25-benchmarks.git
|
||||||
|
cd zgc-jdk25-benchmarks
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
pwsh scripts/run-all.ps1 -JavaHome 'C:\Program Files\Java\jdk-25.0.3'
|
||||||
|
|
||||||
|
# Linux / macOS
|
||||||
|
./scripts/run-all.sh /path/to/jdk-25
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run a single piece — nothing but a JDK 25 is required for anything outside `jmh/`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
javac -d out $(find latency tuning internals analysis -name '*.java')
|
||||||
|
|
||||||
|
java -XX:+UseZGC -Xms2g -Xmx2g -cp out com.ankurm.zgc.latency.LatencyHarness
|
||||||
|
java -XX:+UseG1GC -Xms2g -Xmx2g -cp out com.ankurm.zgc.latency.LatencyHarness
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Headline result
|
||||||
|
|
||||||
|
60 seconds, 20,000 req/s open loop, 2 GB heap, ~585 MB live set, ~328 MB/s allocation rate.
|
||||||
|
Full output: [`results/03-latency-zgc.txt`](results/03-latency-zgc.txt),
|
||||||
|
[`results/03-latency-g1.txt`](results/03-latency-g1.txt).
|
||||||
|
|
||||||
|
| Metric | Generational ZGC | G1 |
|
||||||
|
|---|---|---|
|
||||||
|
| p50 response time | 0.006 ms | **0.005 ms** |
|
||||||
|
| p90 | 0.009 ms | 0.009 ms |
|
||||||
|
| p99 | **0.029 ms** | 8.042 ms |
|
||||||
|
| p99.9 | **1.437 ms** | 95.169 ms |
|
||||||
|
| p99.99 | **23.675 ms** | 126.949 ms |
|
||||||
|
| max | **32.178 ms** | 133.018 ms |
|
||||||
|
| total stop-the-world time | **1.503 ms** | 1,139.624 ms |
|
||||||
|
| longest single pause | **0.054 ms** | 104.449 ms |
|
||||||
|
| Full GCs / evacuation failures | **0 / 0** | 4 / 16 |
|
||||||
|
|
||||||
|
G1 wins the median. ZGC wins everything past p99. The two are indistinguishable up to p90 — which is
|
||||||
|
the part most "ZGC is faster" posts leave out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is in here
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
| Module | What it demonstrates | Deps |
|
||||||
|
|---|---|---|
|
||||||
|
| [`env/Env.java`](env/Env.java) | Environment capture; proves generational mode from JMX bean names | none |
|
||||||
|
| [`latency/`](latency/src/com/ankurm/zgc/latency) | **Open-loop** latency harness with coordinated-omission accounting | none |
|
||||||
|
| [`tuning/AllocationStallDemo.java`](tuning/src/com/ankurm/zgc/tuning/AllocationStallDemo.java) | Provokes and measures ZGC allocation stalls via JFR | none |
|
||||||
|
| [`internals/PageSizeDemo.java`](internals/src/com/ankurm/zgc/internals/PageSizeDemo.java) | ZGC page classes vs G1 humongous, read from the live VM | none |
|
||||||
|
| [`analysis/GcLogParser.java`](analysis/src/com/ankurm/zgc/analysis/GcLogParser.java) | Unified-log parser that keeps pauses and concurrent phases apart | none |
|
||||||
|
| [`jmh/`](jmh/src/main/java/com/ankurm/zgc/jmh) | Throughput and barrier microbenchmarks | JMH 1.37 |
|
||||||
|
|
||||||
|
### JMH benchmarks
|
||||||
|
|
||||||
|
| Class | Isolates |
|
||||||
|
|---|---|
|
||||||
|
| [`AllocationBench`](jmh/src/main/java/com/ankurm/zgc/jmh/AllocationBench.java) | TLAB allocation path at 64 B / 1 KB / 32 KB |
|
||||||
|
| [`LoadBarrierBench`](jmh/src/main/java/com/ankurm/zgc/jmh/LoadBarrierBench.java) | **ZGC's load barrier** — pointer chase vs primitive sum control |
|
||||||
|
| [`StoreBarrierBench`](jmh/src/main/java/com/ankurm/zgc/jmh/StoreBarrierBench.java) | **Write barriers** — old→young vs null vs old→old vs primitive control |
|
||||||
|
| [`PromotionBench`](jmh/src/main/java/com/ankurm/zgc/jmh/PromotionBench.java) | Medium-lived objects; the case where "most objects die young" fails |
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
| Chapter | Covers |
|
||||||
|
|---|---|
|
||||||
|
| [1. The generational model](docs/01-generational-model.md) | JEP 439 → 474 → 490 timeline, what `ZGenerational` does now, why there is no `-Xmn` |
|
||||||
|
| [2. Barriers](docs/02-barriers.md) | Colored pointers, load barrier, store barrier, remembered sets, `ZBufferStoreBarriers` |
|
||||||
|
| [3. Allocation stalls](docs/03-allocation-stalls.md) | **The ZGC failure mode pause charts cannot see**, and the 10 ms JFR threshold that hides it |
|
||||||
|
| [4. Tuning reference](docs/04-tuning-reference.md) | Every ZGC product + diagnostic flag, and the G1 flags that still matter |
|
||||||
|
| [5. Logging and JFR](docs/05-logging-and-jfr.md) | Reading `-Xlog:gc*`, the JMX bean trap, the JFR events worth enabling |
|
||||||
|
| [6. Methodology](docs/06-methodology.md) | Coordinated omission, timer granularity, the JMH `-jvmArgs` fork trap |
|
||||||
|
| [7. Corner cases](docs/07-corner-cases.md) | Compressed oops, large-page fragmentation, virtual thread stacks, containers, `System.gc()` |
|
||||||
|
| [8. Choosing a collector](docs/08-choosing.md) | A decision procedure, sizing guidance, when the answer is neither |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Five findings you will not read elsewhere
|
||||||
|
|
||||||
|
1. **G1's coordinated-omission gap on this workload is 2910x.** A closed-loop harness reports G1's
|
||||||
|
p99.9 as 0.033 ms. It is really 95.169 ms.
|
||||||
|
→ [docs/06-methodology.md §6.1](docs/06-methodology.md)
|
||||||
|
2. **ZGC disables compressed oops.** Every reference is 8 bytes instead of 4, so a reference-dense
|
||||||
|
heap can grow 30–40% on switching collector, before any latency benefit.
|
||||||
|
→ [docs/07-corner-cases.md §7.1](docs/07-corner-cases.md)
|
||||||
|
3. **`jdk.ZAllocationStall` has a 10 ms default JFR threshold.** In a run whose mean stall was
|
||||||
|
3.98 ms, a default profile discards most of them.
|
||||||
|
→ [docs/03-allocation-stalls.md §3.3](docs/03-allocation-stalls.md)
|
||||||
|
4. **`new byte[4*1024*1024]` gets a ZGC Large page, not a Medium one.** The 16-byte array header
|
||||||
|
pushes it over the medium-object limit, and it consumes an 8 MB page.
|
||||||
|
→ [docs/07-corner-cases.md §7.2](docs/07-corner-cases.md)
|
||||||
|
5. **`SoftMaxHeapSize` and `ZAllocationSpikeTolerance` did nothing under sustained overload.**
|
||||||
|
Measured: 32,200 / 31,967 / 32,442 stalls across baseline and both mitigations. They buy headroom
|
||||||
|
for *spikes*; they cannot conjure heap that does not exist.
|
||||||
|
→ [results/04-stalls-zgc-softmax.txt](results/04-stalls-zgc-softmax.txt)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference machine
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| CPU | AMD Ryzen 5 5600U, 6 cores / 12 threads |
|
||||||
|
| RAM | 15.3 GB |
|
||||||
|
| OS | Windows 11 Home |
|
||||||
|
| JDK | `java 25.0.3+9-LTS-195` (Oracle) |
|
||||||
|
| Heap | `-Xms2g -Xmx2g` for all latency runs; `-Xms512m -Xmx512m` for stall runs |
|
||||||
|
|
||||||
|
A laptop, not a server. The absolute numbers are illustrative; the *relationships* are what
|
||||||
|
generalise. Run it on your own hardware — that is what it is for.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
MIT. See [LICENSE](LICENSE).
|
||||||
184
analysis/src/com/ankurm/zgc/analysis/GcLogParser.java
Normal file
184
analysis/src/com/ankurm/zgc/analysis/GcLogParser.java
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package com.ankurm.zgc.analysis;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a JDK unified GC log into a pause distribution, for either ZGC or G1.
|
||||||
|
*
|
||||||
|
* <p><b>Why not just use a hosted log analyser?</b> Because the interesting question here is
|
||||||
|
* cross-collector, and the two logs mean different things by the word "pause":
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>G1</b> emits one line per stop-the-world event:
|
||||||
|
* {@code GC(12) Pause Young (Normal) (G1 Evacuation Pause) 1234M->456M(2048M) 12.345ms}.
|
||||||
|
* Summing these gives total application freeze time, which is what you want.</li>
|
||||||
|
* <li><b>ZGC</b> emits <em>three tiny pauses per cycle</em> ({@code Pause Mark Start},
|
||||||
|
* {@code Pause Mark End}, {@code Pause Relocate Start}) plus a long concurrent
|
||||||
|
* {@code Minor/Major Collection ... 0.850s} line that is <em>not</em> a pause at all.
|
||||||
|
* Summing everything with a duration in it -- which is what a naive script does -- makes ZGC
|
||||||
|
* look catastrophically worse than G1. This parser keeps the two categories apart.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>It also counts the lines that matter and are easy to miss: ZGC {@code Allocation Stall}
|
||||||
|
* entries, and G1 {@code To-space exhausted} / {@code Evacuation Failure} / {@code Pause Full}
|
||||||
|
* entries. Those are each collector's "we lost the race" signal.
|
||||||
|
*
|
||||||
|
* <h2>Producing a log this can read</h2>
|
||||||
|
* <pre>
|
||||||
|
* java -XX:+UseZGC -Xlog:gc*:file=zgc.log:time,uptime,level,tags MyApp
|
||||||
|
* java -cp out com.ankurm.zgc.analysis.GcLogParser zgc.log
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public final class GcLogParser {
|
||||||
|
|
||||||
|
/** Any "... 12.345ms" or "... 0.850s" trailing duration. */
|
||||||
|
private static final Pattern DURATION =
|
||||||
|
Pattern.compile("(\\d+[.,]\\d+)\\s*(ms|s|us)\\s*$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the {@code GC(n)} prefix, an optional ZGC generation marker ({@code y:} / {@code o:}),
|
||||||
|
* and then the phase name.
|
||||||
|
*
|
||||||
|
* <p>The phase name is deliberately <em>not</em> matched with a single greedy pattern. G1's
|
||||||
|
* lines put heap occupancy between the phase name and the duration:
|
||||||
|
*
|
||||||
|
* <pre>GC(12) Pause Young (Normal) (G1 Evacuation Pause) 1180M->402M(2048M) 16.784ms</pre>
|
||||||
|
*
|
||||||
|
* while ZGC's do not:
|
||||||
|
*
|
||||||
|
* <pre>GC(41) y: Concurrent Mark 32.109ms</pre>
|
||||||
|
*
|
||||||
|
* An earlier version of this parser required the phase name to be followed directly by the
|
||||||
|
* duration, which silently matched every ZGC line and no G1 line at all -- and then reported
|
||||||
|
* "no pauses found" for G1, which is the most flattering possible bug for G1. The phase name is
|
||||||
|
* therefore taken as the leading run of letters, spaces and parenthesised qualifiers, and the
|
||||||
|
* duration is taken separately from the end of the line.
|
||||||
|
*/
|
||||||
|
private static final Pattern PHASE_PREFIX =
|
||||||
|
Pattern.compile("GC\\((\\d+)\\)\\s+(?:[YyOo]:\\s*)?(Pause|Concurrent|Garbage Collection)\\b([A-Za-z ]*(?:\\([^)0-9]*\\)\\s*)*)");
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
if (args.length < 1) {
|
||||||
|
System.err.println("usage: GcLogParser <unified-gc-log> [...]");
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
for (String arg : args) {
|
||||||
|
analyse(Path.of(arg));
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void analyse(Path log) throws IOException {
|
||||||
|
if (!Files.exists(log)) {
|
||||||
|
System.out.println("missing log: " + log);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> lines = Files.readAllLines(log);
|
||||||
|
|
||||||
|
List<Double> pauseMillis = new ArrayList<>();
|
||||||
|
Map<String, List<Double>> pausesByPhase = new LinkedHashMap<>();
|
||||||
|
Map<String, List<Double>> concurrentByPhase = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
int allocationStalls = 0;
|
||||||
|
int evacuationFailures = 0;
|
||||||
|
int fullGcs = 0;
|
||||||
|
int youngCollections = 0;
|
||||||
|
int oldCollections = 0;
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.contains("Allocation Stall")) allocationStalls++;
|
||||||
|
if (line.contains("To-space exhausted") || line.contains("Evacuation Failure")) evacuationFailures++;
|
||||||
|
if (line.contains("Pause Full")) fullGcs++;
|
||||||
|
if (line.contains("Minor Collection") || line.contains("Pause Young")) youngCollections++;
|
||||||
|
if (line.contains("Major Collection") || line.contains("Pause Old")) oldCollections++;
|
||||||
|
|
||||||
|
Matcher phase = PHASE_PREFIX.matcher(line);
|
||||||
|
if (!phase.find()) continue;
|
||||||
|
|
||||||
|
double ms = parseTrailingMillis(line);
|
||||||
|
if (ms < 0) continue; // a line with no trailing duration -- skip
|
||||||
|
|
||||||
|
String kind = phase.group(2); // "Pause" | "Concurrent" | "Garbage Collection"
|
||||||
|
String name = (kind + " " + phase.group(3)).replaceAll("\\s+", " ").trim();
|
||||||
|
|
||||||
|
if (kind.equals("Pause")) {
|
||||||
|
pauseMillis.add(ms);
|
||||||
|
pausesByPhase.computeIfAbsent(name, k -> new ArrayList<>()).add(ms);
|
||||||
|
} else if (kind.equals("Concurrent")) {
|
||||||
|
concurrentByPhase.computeIfAbsent(name, k -> new ArrayList<>()).add(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("=".repeat(78));
|
||||||
|
System.out.println("GC log analysis: " + log.getFileName());
|
||||||
|
System.out.println("=".repeat(78));
|
||||||
|
System.out.printf("lines parsed : %,d%n", lines.size());
|
||||||
|
System.out.printf("young / minor cycles : %,d%n", youngCollections);
|
||||||
|
System.out.printf("old / major cycles : %,d%n", oldCollections);
|
||||||
|
System.out.printf("ZGC allocation stalls : %,d%n", allocationStalls);
|
||||||
|
System.out.printf("G1 evacuation failures : %,d%n", evacuationFailures);
|
||||||
|
System.out.printf("full GCs : %,d%n", fullGcs);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
if (pauseMillis.isEmpty()) {
|
||||||
|
System.out.println("No stop-the-world pause lines found. Was the log written with -Xlog:gc* ?");
|
||||||
|
} else {
|
||||||
|
Collections.sort(pauseMillis);
|
||||||
|
double total = pauseMillis.stream().mapToDouble(Double::doubleValue).sum();
|
||||||
|
System.out.println("STOP-THE-WORLD PAUSES (this is application freeze time)");
|
||||||
|
System.out.printf(" count : %,d%n", pauseMillis.size());
|
||||||
|
System.out.printf(" total : %,.3f ms%n", total);
|
||||||
|
System.out.printf(" mean : %,.3f ms%n", total / pauseMillis.size());
|
||||||
|
System.out.printf(" p50 : %,.3f ms%n", percentile(pauseMillis, 50));
|
||||||
|
System.out.printf(" p99 : %,.3f ms%n", percentile(pauseMillis, 99));
|
||||||
|
System.out.printf(" max : %,.3f ms%n", pauseMillis.getLast());
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" by phase");
|
||||||
|
pausesByPhase.forEach((phase, values) -> {
|
||||||
|
double sum = values.stream().mapToDouble(Double::doubleValue).sum();
|
||||||
|
System.out.printf(" %-28s n=%-6d total=%8.3f ms max=%7.3f ms%n",
|
||||||
|
phase, values.size(), sum, Collections.max(values));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!concurrentByPhase.isEmpty()) {
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("CONCURRENT PHASES (application keeps running -- NOT freeze time)");
|
||||||
|
concurrentByPhase.forEach((phase, values) -> {
|
||||||
|
double sum = values.stream().mapToDouble(Double::doubleValue).sum();
|
||||||
|
System.out.printf(" %-28s n=%-6d total=%8.3f ms max=%7.3f ms%n",
|
||||||
|
phase, values.size(), sum, Collections.max(values));
|
||||||
|
});
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" Adding the block above to the pause block would be a category error:");
|
||||||
|
System.out.println(" concurrent time overlaps application execution by design.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalises a trailing "12.345ms" / "0.850s" / "812us" to milliseconds. */
|
||||||
|
private static double parseTrailingMillis(String line) {
|
||||||
|
Matcher m = DURATION.matcher(line.strip());
|
||||||
|
if (!m.find()) return -1;
|
||||||
|
double value = Double.parseDouble(m.group(1).replace(',', '.'));
|
||||||
|
return switch (m.group(2)) {
|
||||||
|
case "s" -> value * 1000.0;
|
||||||
|
case "us" -> value / 1000.0;
|
||||||
|
default -> value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double percentile(List<Double> sorted, double p) {
|
||||||
|
if (sorted.isEmpty()) return 0;
|
||||||
|
int idx = (int) Math.ceil(p / 100.0 * sorted.size()) - 1;
|
||||||
|
return sorted.get(Math.clamp(idx, 0, sorted.size() - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
115
docs/01-generational-model.md
Normal file
115
docs/01-generational-model.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# 1. The generational model, and what JDK 25 actually changed
|
||||||
|
|
||||||
|
> Companion notes for [Generational ZGC on JDK 25: Benchmarks vs G1](https://ankurm.com/generational-zgc-jdk-25-vs-g1/).
|
||||||
|
> Everything here is verified against `java 25.0.3+9-LTS-195`.
|
||||||
|
|
||||||
|
## 1.1 The one-paragraph history
|
||||||
|
|
||||||
|
| Release | What happened |
|
||||||
|
|---|---|
|
||||||
|
| JDK 11 | ZGC arrives as an experimental, **non-generational** collector (JEP 333). One generation, whole-heap marking every cycle. |
|
||||||
|
| JDK 15 | ZGC becomes a production feature (JEP 377). |
|
||||||
|
| JDK 21 | **Generational ZGC** ships as an opt-in mode behind `-XX:+ZGenerational` (JEP 439). |
|
||||||
|
| JDK 23 | Generational becomes the **default** (JEP 474). `-XX:-ZGenerational` still gets you the old one. |
|
||||||
|
| JDK 24 | The non-generational mode is **removed** (JEP 490). `ZGenerational` becomes an obsolete flag. |
|
||||||
|
| JDK 25 | First **LTS** where ZGC is generational-only. This is the release most teams will actually adopt. |
|
||||||
|
|
||||||
|
Verify the removal yourself — the JVM says so out loud:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ java -XX:+UseZGC -XX:+ZGenerational -version
|
||||||
|
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option ZGenerational; support was removed in 24.0
|
||||||
|
java version "25.0.3" 2026-04-21 LTS
|
||||||
|
```
|
||||||
|
|
||||||
|
That warning matters operationally: if your start-up scripts still carry `-XX:+ZGenerational` from a
|
||||||
|
JDK 21 experiment, JDK 25 will *not* fail fast. It warns and continues. Grep your deployment
|
||||||
|
manifests.
|
||||||
|
|
||||||
|
## 1.2 What "generational" buys, concretely
|
||||||
|
|
||||||
|
The weak generational hypothesis: most objects die young. A collector that exploits it can reclaim
|
||||||
|
the young generation without looking at the old one.
|
||||||
|
|
||||||
|
Non-generational ZGC could not do that. Every cycle marked the **entire** heap. With a 600 MB live
|
||||||
|
set and a 300 MB/s allocation rate, that means repeatedly tracing 600 MB of objects that you
|
||||||
|
already know are alive, in order to reclaim garbage that lives in a few megabytes of recently
|
||||||
|
allocated pages.
|
||||||
|
|
||||||
|
Generational ZGC splits this into:
|
||||||
|
|
||||||
|
- a **minor (young) collection** that marks and relocates only young pages, and
|
||||||
|
- a **major (old) collection** that additionally handles the old generation, run far less often.
|
||||||
|
|
||||||
|
You can see the split in the JMX bean names, which is the cheapest possible proof of what you are
|
||||||
|
running:
|
||||||
|
|
||||||
|
```console
|
||||||
|
collector : ZGC Minor Cycles + ZGC Minor Pauses + ZGC Major Cycles + ZGC Major Pauses
|
||||||
|
```
|
||||||
|
|
||||||
|
Four beans, not two. Under non-generational ZGC there was one cycle bean and one pause bean.
|
||||||
|
`env/Env.java` in this repository prints exactly this and derives `generational? yes`.
|
||||||
|
|
||||||
|
## 1.3 The cost of the split: you now need a write barrier
|
||||||
|
|
||||||
|
There is no free lunch. To collect young without scanning old, the collector must know about every
|
||||||
|
**old-to-young reference**. Those are created by ordinary application code:
|
||||||
|
|
||||||
|
```java
|
||||||
|
oldCache[i] = new Payload(); // an old object now points at a brand-new young object
|
||||||
|
```
|
||||||
|
|
||||||
|
If a young collection ignored `oldCache`, it would conclude `new Payload()` is unreachable and free
|
||||||
|
a live object.
|
||||||
|
|
||||||
|
So generational ZGC added a **store barrier** — code the JIT injects around reference stores, which
|
||||||
|
records the cross-generational edge. Non-generational ZGC had *no* store barrier at all; it only
|
||||||
|
had a load barrier. This is the single biggest structural change in JEP 439, and it is why
|
||||||
|
generational ZGC has slightly different throughput characteristics from the old one on
|
||||||
|
store-heavy code. See [02-barriers.md](02-barriers.md).
|
||||||
|
|
||||||
|
You can see the mechanism named in the VM's own diagnostic flags:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ java -XX:+UseZGC -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version | findstr Z
|
||||||
|
bool ZBufferStoreBarriers = true {diagnostic} {default}
|
||||||
|
int ZTenuringThreshold = -1 {diagnostic} {default}
|
||||||
|
bool ZUseMediumPageSizeRange = true {diagnostic} {default}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ZBufferStoreBarriers` — the store barrier does not update a remembered set inline; it appends to
|
||||||
|
a per-thread buffer that is drained later. That is what keeps the barrier cheap.
|
||||||
|
- `ZTenuringThreshold = -1` — **adaptive**. ZGC ages objects like any generational collector, but
|
||||||
|
unlike G1's `MaxTenuringThreshold` (default 15, a documented product flag), ZGC's is a diagnostic
|
||||||
|
flag that defaults to "let the collector decide". Pinning it is almost always a mistake; knowing
|
||||||
|
it exists is occasionally the answer to "why did my promotion rate change after an upgrade".
|
||||||
|
|
||||||
|
## 1.4 Where the generations physically live
|
||||||
|
|
||||||
|
ZGC does not have a contiguous eden / survivor / old layout the way Parallel or (loosely) G1 does.
|
||||||
|
Generation membership is a property of a **page**, and pages are 2 MB / medium / large units
|
||||||
|
scattered across the reserved address space. A young page whose objects survive enough cycles is
|
||||||
|
*promoted* by relocating its live objects into an old page.
|
||||||
|
|
||||||
|
This has a practical consequence people trip over: **there is no `-Xmn` for ZGC**. You cannot size
|
||||||
|
the young generation. ZGC decides how much of the heap is young dynamically, based on allocation
|
||||||
|
rate and how much headroom `SoftMaxHeapSize` leaves it. If you are used to tuning G1 with
|
||||||
|
`-XX:NewRatio` or `-XX:G1NewSizePercent`, that entire toolbox is gone. The ZGC equivalent of "give
|
||||||
|
the young generation more room" is "give the heap more room, or lower `SoftMaxHeapSize` so
|
||||||
|
collection starts earlier".
|
||||||
|
|
||||||
|
## 1.5 What this means for a migration
|
||||||
|
|
||||||
|
| If you relied on… | On generational ZGC you get… |
|
||||||
|
|---|---|
|
||||||
|
| `-Xmn`, `-XX:NewRatio`, `-XX:SurvivorRatio` | Nothing. Ignored or unavailable. Size the whole heap instead. |
|
||||||
|
| `-XX:MaxTenuringThreshold` | `ZTenuringThreshold`, diagnostic, adaptive by default. |
|
||||||
|
| `-XX:+ZGenerational` | An obsolete-flag warning. Remove it. |
|
||||||
|
| GC pause charts as your latency SLI | A misleading green dashboard. See [03-allocation-stalls.md](03-allocation-stalls.md). |
|
||||||
|
| `-XX:+UseStringDeduplication` | Still supported on ZGC (since JDK 18), not G1-only any more. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Next: [02-barriers.md](02-barriers.md) — colored pointers, load barriers, store barriers and
|
||||||
|
remembered sets, with the microbenchmark that prices each one.
|
||||||
106
docs/02-barriers.md
Normal file
106
docs/02-barriers.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# 2. Colored pointers, load barriers, store barriers, remembered sets
|
||||||
|
|
||||||
|
> Runnable companions: [`jmh/.../LoadBarrierBench.java`](../jmh/src/main/java/com/ankurm/zgc/jmh/LoadBarrierBench.java),
|
||||||
|
> [`jmh/.../StoreBarrierBench.java`](../jmh/src/main/java/com/ankurm/zgc/jmh/StoreBarrierBench.java)
|
||||||
|
|
||||||
|
This is the chapter that explains *why* the numbers look the way they do. Every throughput
|
||||||
|
difference between ZGC and G1 traces back to one of the mechanisms below.
|
||||||
|
|
||||||
|
## 2.1 Colored pointers
|
||||||
|
|
||||||
|
A 64-bit JVM does not need 64 bits to address the heap. ZGC uses the spare high bits of every heap
|
||||||
|
reference as metadata — the "color". The colors encode facts like *has this pointer been marked in
|
||||||
|
the current young cycle?* and *has the object it points at been relocated?*
|
||||||
|
|
||||||
|
The consequence is that **the pointer, not a side table, carries the collector's state**. Two
|
||||||
|
objects can hold references to the same object with different colors, and each will be fixed up
|
||||||
|
independently the first time it is read.
|
||||||
|
|
||||||
|
This is the trick that lets ZGC relocate objects concurrently. Traditional compacting collectors
|
||||||
|
must stop the world to fix up every reference to a moved object. ZGC does not: it moves the object,
|
||||||
|
records a forwarding entry, and lets each stale reference repair itself on next read.
|
||||||
|
|
||||||
|
> **Multi-mapping vs. multi-mapping-free.** Older ZGC mapped the same physical heap at several
|
||||||
|
> virtual addresses so that colored pointers could be dereferenced directly. Modern ZGC (from JDK
|
||||||
|
> 22's "colored roots"/load-barrier rework onward) masks the color in the barrier instead. Practical
|
||||||
|
> effect: on Linux, `RSS` no longer looks like 3x the heap in tooling that misreads multi-mapped
|
||||||
|
> regions. If you have old runbooks explaining that "ZGC reports 3x memory, ignore it", they are
|
||||||
|
> obsolete.
|
||||||
|
|
||||||
|
## 2.2 The load barrier
|
||||||
|
|
||||||
|
Every time your code reads a **reference field**, the JIT emits a short sequence: test the color
|
||||||
|
bits, and on the slow path fix the pointer. Primitive loads (`int`, `long`, elements of `int[]`) are
|
||||||
|
untouched — a common misconception is that ZGC taxes all memory access. It does not. It taxes
|
||||||
|
reference loads.
|
||||||
|
|
||||||
|
`LoadBarrierBench` prices this by comparing two walks over the same number of elements:
|
||||||
|
|
||||||
|
| Benchmark | What it does | Barrier? |
|
||||||
|
|---|---|---|
|
||||||
|
| `chaseReferences` | walks a shuffled 1M-node linked structure, one reference load per step | yes, one per step |
|
||||||
|
| `sumPrimitives` | sums an `int[]` of the same length | none |
|
||||||
|
| `readSameReferenceRepeatedly` | reads one already-good reference 1024 times | yes, but always fast path |
|
||||||
|
|
||||||
|
The third one is the interesting control: it measures the **floor** of the barrier — what you pay
|
||||||
|
when there is nothing to fix. If `readSameReferenceRepeatedly` is close between ZGC and G1, the
|
||||||
|
fast path is nearly free and any gap in `chaseReferences` is the slow path plus cache effects.
|
||||||
|
|
||||||
|
Two things move these numbers a lot, and both are worth measuring on your own workload:
|
||||||
|
|
||||||
|
- **Whether a GC cycle is in progress.** The barrier does strictly more work during marking and
|
||||||
|
relocation. Force cycles with `-XX:ZCollectionInterval=1` to see the sustained-load case rather
|
||||||
|
than the idle-heap case.
|
||||||
|
- **Whether the walk is cache-resident.** A sequential walk over a small structure is dominated by
|
||||||
|
the prefetcher and hides the barrier entirely. That is why the benchmark shuffles the node order —
|
||||||
|
a "random" pointer chase that is secretly sequential measures the memory subsystem, not the GC.
|
||||||
|
|
||||||
|
## 2.3 The store barrier and remembered sets
|
||||||
|
|
||||||
|
Generational collection requires knowing about old-to-young references (see
|
||||||
|
[01-generational-model.md](01-generational-model.md) §1.3). The two collectors solve it differently
|
||||||
|
and the difference is measurable.
|
||||||
|
|
||||||
|
**G1: card table + concurrent refinement.**
|
||||||
|
The heap is divided into 512-byte *cards*. A reference store dirties the card containing the
|
||||||
|
modified field. Concurrent refinement threads later scan dirty cards and update per-region
|
||||||
|
*remembered sets* (which regions point into me?). G1's barrier is a two-part affair — a pre-write
|
||||||
|
barrier for SATB marking and a post-write barrier for card marking — and has historically been the
|
||||||
|
more expensive of the two collectors' barriers. JDK 24 landed *late barrier expansion* for G1
|
||||||
|
(JDK-8342382), which defers expanding the barrier into IR until late in C2 so the optimiser can see
|
||||||
|
through more of the surrounding code; JDK 25 continued reducing remembered-set memory.
|
||||||
|
|
||||||
|
**ZGC: store barrier + per-page remembered sets.**
|
||||||
|
Generational ZGC's store barrier appends to a thread-local buffer (`ZBufferStoreBarriers=true`),
|
||||||
|
which is drained into per-page remembered-set bitmaps. The bitmaps are **double-buffered**: one is
|
||||||
|
being written by the application while the other is consumed by the collector, which is how ZGC
|
||||||
|
avoids a stop-the-world handoff.
|
||||||
|
|
||||||
|
`StoreBarrierBench` separates "the barrier code ran" from "the barrier had work to do":
|
||||||
|
|
||||||
|
| Benchmark | Store | Creates old→young edge? |
|
||||||
|
|---|---|---|
|
||||||
|
| `storeOldToYoung` | `oldArray[i] = new Object()` | yes |
|
||||||
|
| `storeNullIntoOld` | `oldArray[i] = null` | no — barrier runs, records nothing |
|
||||||
|
| `storeOldToOld` | `oldArray[i] = oldArray` | no |
|
||||||
|
| `storePrimitiveIntoOld` | `oldIntArray[i] = i` | not a reference store; no barrier at all |
|
||||||
|
|
||||||
|
The benchmark walks slots with a stride of 4099 rather than sequentially. Sequential stores hit the
|
||||||
|
same card / same page repeatedly, and both collectors have an "already dirty" fast path that would
|
||||||
|
hide most of the cost. This is the single most common way a store-barrier microbenchmark
|
||||||
|
accidentally measures nothing.
|
||||||
|
|
||||||
|
## 2.4 Why this shows up as a throughput gap, not a latency gap
|
||||||
|
|
||||||
|
Barriers cost throughput: a few extra instructions on a very hot path. They do **not** cost latency,
|
||||||
|
because they are spread evenly across execution rather than concentrated into a pause.
|
||||||
|
|
||||||
|
That is the whole trade in one sentence: **ZGC converts a rare, large, correlated cost (a
|
||||||
|
stop-the-world pause) into a constant, small, uncorrelated cost (barrier work on every reference
|
||||||
|
access).** Whether that is a good deal depends entirely on whether your SLO is expressed as a mean
|
||||||
|
or as a tail percentile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Next: [03-allocation-stalls.md](03-allocation-stalls.md) — the ZGC failure mode that pause charts
|
||||||
|
cannot see.
|
||||||
161
docs/03-allocation-stalls.md
Normal file
161
docs/03-allocation-stalls.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# 3. Allocation stalls — the ZGC failure mode your dashboard cannot see
|
||||||
|
|
||||||
|
> Runnable companion: [`tuning/.../AllocationStallDemo.java`](../tuning/src/com/ankurm/zgc/tuning/AllocationStallDemo.java)
|
||||||
|
|
||||||
|
## 3.1 The claim, and why it is misleading
|
||||||
|
|
||||||
|
Every ZGC article ends with the same chart: pause times, flat, under a millisecond, at any heap
|
||||||
|
size. The chart is accurate. The implied conclusion — *therefore ZGC has no latency problem* — is
|
||||||
|
not, because **on ZGC, pauses are not where latency comes from.**
|
||||||
|
|
||||||
|
ZGC's stop-the-world pauses are O(number of GC roots). They do not scale with heap size or live-set
|
||||||
|
size, and they stay in the tens of microseconds. There are three of them per cycle: `Pause Mark
|
||||||
|
Start`, `Pause Mark End`, `Pause Relocate Start`.
|
||||||
|
|
||||||
|
What actually hurts is different. When the application allocates faster than the collector can
|
||||||
|
reclaim, a thread that requests memory and cannot be given a page is **stalled** until the collector
|
||||||
|
frees one. That stall:
|
||||||
|
|
||||||
|
- can last **tens or hundreds of milliseconds**,
|
||||||
|
- blocks only the allocating thread, not the whole JVM,
|
||||||
|
- and therefore **never appears in a pause-time metric.**
|
||||||
|
|
||||||
|
A ZGC service can report a p99.99 pause of 0.2 ms and a p99.99 request latency of 400 ms
|
||||||
|
simultaneously. Both numbers are correct. If your only GC SLI is pause time, you will never find the
|
||||||
|
second one.
|
||||||
|
|
||||||
|
## 3.2 Seeing it
|
||||||
|
|
||||||
|
`AllocationStallDemo` starts a JFR recording, deliberately drives a small heap into the ground, then
|
||||||
|
reads its own recording back. Measured on this repository's reference machine (Ryzen 5 5600U,
|
||||||
|
Windows 11, JDK 25.0.3), 512 MB heap, 15 seconds of allocation:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ java -XX:+UseZGC -Xms512m -Xmx512m -Dseconds=15 \
|
||||||
|
-cp out com.ankurm.zgc.tuning.AllocationStallDemo
|
||||||
|
|
||||||
|
young collections : 1,319
|
||||||
|
old collections : 164
|
||||||
|
page allocations : 30,768
|
||||||
|
ALLOCATION STALLS : 25,343
|
||||||
|
|
||||||
|
total time threads spent stalled : 100,989 ms
|
||||||
|
mean stall : 3.98 ms
|
||||||
|
longest stall : 49.01 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Read that again: **101 seconds of cumulative stalled thread time inside a 15-second run.** Twelve
|
||||||
|
allocator threads, each spending a large fraction of the run waiting for memory. The pause chart for
|
||||||
|
this run is a flat line near zero.
|
||||||
|
|
||||||
|
## 3.3 The threshold that hides them
|
||||||
|
|
||||||
|
`jdk.ZAllocationStall` has a **default JFR threshold of 10 ms**. Anything shorter is not recorded.
|
||||||
|
In the run above the *mean* stall was 3.98 ms — meaning a default JFR profile would have discarded
|
||||||
|
the majority of them and reported a much healthier picture.
|
||||||
|
|
||||||
|
If you take one operational thing from this repository, take this:
|
||||||
|
|
||||||
|
```java
|
||||||
|
recording.enable("jdk.ZAllocationStall").withoutThreshold();
|
||||||
|
```
|
||||||
|
|
||||||
|
or in a `.jfc` settings file:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<event name="jdk.ZAllocationStall">
|
||||||
|
<setting name="enabled">true</setting>
|
||||||
|
<setting name="threshold">0 ms</setting>
|
||||||
|
<setting name="stackTrace">true</setting>
|
||||||
|
</event>
|
||||||
|
```
|
||||||
|
|
||||||
|
With stack traces on, you get the exact allocation site that stalled — which is usually far more
|
||||||
|
actionable than any GC log line.
|
||||||
|
|
||||||
|
The same events are visible in the unified log without JFR:
|
||||||
|
|
||||||
|
```console
|
||||||
|
-Xlog:gc+alloc=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
produces `Allocation Stall` lines. The `GcLogParser` in this repository counts them.
|
||||||
|
|
||||||
|
## 3.4 The stalls have a *shape*
|
||||||
|
|
||||||
|
The demo groups stalls by ZGC page class, which turns "we are out of memory" into a diagnosis:
|
||||||
|
|
||||||
|
- **Small** page stalls — ordinary allocation pressure. The fix is more heap, an earlier collection
|
||||||
|
trigger, or allocating less.
|
||||||
|
- **Medium** page stalls — objects in the 256 KB–4 MB band. Often a buffer-pool or serialization
|
||||||
|
layer. Frequently fixable in application code.
|
||||||
|
- **Large** page stalls — one page per object. ZGC does not relocate large pages, so a heap with
|
||||||
|
many long-lived large objects fragments in a way ZGC cannot compact away.
|
||||||
|
|
||||||
|
A run dominated by Large-page stalls has a completely different remedy from one dominated by Small,
|
||||||
|
and no pause-time chart distinguishes them.
|
||||||
|
|
||||||
|
## 3.5 Mitigations, in the order you should try them
|
||||||
|
|
||||||
|
### `SoftMaxHeapSize` — start collecting earlier
|
||||||
|
|
||||||
|
```
|
||||||
|
-XX:SoftMaxHeapSize=400m # with -Xmx512m
|
||||||
|
```
|
||||||
|
|
||||||
|
ZGC's heuristic aims to keep the heap under `SoftMaxHeapSize` but is *allowed* to exceed it, up to
|
||||||
|
`-Xmx`, rather than stall the application. Setting it below `-Xmx` therefore creates a deliberate
|
||||||
|
reserve: normal operation targets the soft limit, and a spike can eat into the gap instead of
|
||||||
|
hitting a wall. On JDK 25 it defaults to `-Xmx`, i.e. no reserve at all.
|
||||||
|
|
||||||
|
It is a `manageable` flag, so you can change it on a running JVM — genuinely useful during an
|
||||||
|
incident:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ jcmd <pid> VM.set_flag SoftMaxHeapSize 400m
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ZAllocationSpikeTolerance` — assume bigger spikes
|
||||||
|
|
||||||
|
```
|
||||||
|
-XX:ZAllocationSpikeTolerance=5 # default 2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
ZGC predicts when to start a cycle from the observed allocation rate multiplied by this tolerance
|
||||||
|
factor. The default of 2.0 assumes allocation can double. A bursty service — one that goes from idle
|
||||||
|
to full throttle in a second — routinely violates that, and the collector starts a cycle too late.
|
||||||
|
Raising the tolerance makes ZGC start earlier and more often: you pay CPU to buy headroom.
|
||||||
|
|
||||||
|
### `-XX:ZCollectionIntervalMinor` / `Major` — collect on a clock
|
||||||
|
|
||||||
|
Both default to `-1.0` (disabled). Setting `ZCollectionIntervalMinor=1` forces a young collection
|
||||||
|
every second regardless of allocation rate. This is a blunt instrument, but it is the right one for
|
||||||
|
a service with a very spiky duty cycle where the rate-based heuristic keeps being surprised.
|
||||||
|
|
||||||
|
### More heap
|
||||||
|
|
||||||
|
The honest answer, when you can afford it. ZGC trades memory for latency by design; a ZGC heap sized
|
||||||
|
like a G1 heap is a ZGC heap that stalls.
|
||||||
|
|
||||||
|
## 3.6 What the same pressure looks like on G1
|
||||||
|
|
||||||
|
Run the identical demo under `-XX:+UseG1GC` and there are no allocation stalls, because G1 does not
|
||||||
|
have that mechanism. Instead you get **to-space exhaustion** (an evacuation failure) followed by a
|
||||||
|
**Full GC** — a genuine, whole-application, stop-the-world compaction that will show up in your
|
||||||
|
pause chart as a multi-hundred-millisecond spike.
|
||||||
|
|
||||||
|
So the failure modes are mirror images:
|
||||||
|
|
||||||
|
| | ZGC | G1 |
|
||||||
|
|---|---|---|
|
||||||
|
| Symptom under memory pressure | allocation stalls | evacuation failure → Full GC |
|
||||||
|
| Affects | the allocating thread(s) | every thread |
|
||||||
|
| Visible in pause metrics | **no** | yes |
|
||||||
|
| Typical magnitude here | 4 ms mean, 49 ms max, thousands of them | hundreds of ms, few of them |
|
||||||
|
|
||||||
|
G1's failure is louder and easier to find. ZGC's is quieter and easier to live with — provided you
|
||||||
|
are actually measuring it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Next: [04-tuning-reference.md](04-tuning-reference.md).
|
||||||
101
docs/04-tuning-reference.md
Normal file
101
docs/04-tuning-reference.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# 4. Tuning reference (JDK 25)
|
||||||
|
|
||||||
|
Everything below was read out of a live `java 25.0.3+9-LTS-195` with
|
||||||
|
`-XX:+PrintFlagsFinal` and `HotSpotDiagnosticMXBean`. Raw dumps:
|
||||||
|
[`results/02-flags-zgc.txt`](../results/02-flags-zgc.txt),
|
||||||
|
[`results/02-flags-g1.txt`](../results/02-flags-g1.txt).
|
||||||
|
|
||||||
|
The general rule for ZGC is unusual and worth stating plainly: **there is almost nothing to tune.**
|
||||||
|
The design goal was to remove knobs, not add them. If you find yourself reaching for a fourth flag,
|
||||||
|
the answer is usually heap size or application allocation behaviour.
|
||||||
|
|
||||||
|
## 4.1 ZGC — product flags (supported, safe to use)
|
||||||
|
|
||||||
|
| Flag | Default (2 GB heap) | What it does | When to touch it |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `-XX:+UseZGC` | off | Selects ZGC. Always generational on JDK 24+. | — |
|
||||||
|
| `-XX:SoftMaxHeapSize` | = `-Xmx` | Soft target the GC heuristic aims for; may be exceeded up to `-Xmx` rather than stall. **`manageable`** — changeable at runtime via `jcmd VM.set_flag`. | Set below `-Xmx` to create spike headroom. The single highest-value ZGC flag. |
|
||||||
|
| `-XX:ZAllocationSpikeTolerance` | `2.0` | Multiplier on observed allocation rate when predicting when to start a cycle. | Raise (3–5) for bursty services that stall on ramp-up. |
|
||||||
|
| `-XX:ZCollectionIntervalMinor` | `-1.0` (off) | Force a young cycle every N seconds. | Very spiky duty cycles where rate prediction keeps failing. |
|
||||||
|
| `-XX:ZCollectionIntervalMajor` | `-1.0` (off) | Force an old cycle every N seconds. | Slow, steady old-gen growth you want amortised. |
|
||||||
|
| `-XX:ZCollectionInterval` | `0.0` (off) | Legacy combined interval. | Prefer the Minor/Major pair. |
|
||||||
|
| `-XX:ZCollectionIntervalOnly` | `false` | Collect *only* on the interval, ignoring the allocation-rate heuristic. | Benchmarks and reproducible experiments. Not production. |
|
||||||
|
| `-XX:ZFragmentationLimit` | `5.0` (%) | A page is added to the relocation set when this much of it is garbage. | Lower = more aggressive compaction, more CPU. Rarely worth changing. |
|
||||||
|
| `-XX:ZYoungCompactionLimit` | `25.0` (%) | Equivalent threshold for young pages. | Rarely. |
|
||||||
|
| `-XX:+ZUncommit` | `true`, **but `false` when `-Xms == -Xmx`** | Return unused heap to the OS. | Leave on. Note the automatic disable — see §4.4. |
|
||||||
|
| `-XX:ZUncommitDelay` | `300` (s) | How long a page must stay unused before being uncommitted. | Lower it in bursty container workloads to give memory back sooner. |
|
||||||
|
|
||||||
|
## 4.2 ZGC — diagnostic flags (need `-XX:+UnlockDiagnosticVMOptions`)
|
||||||
|
|
||||||
|
These appear in no tuning guide. You should almost never set them. You should know they exist,
|
||||||
|
because they name the mechanisms.
|
||||||
|
|
||||||
|
| Flag | Default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `ZBufferStoreBarriers` | `true` | The generational store barrier appends to a per-thread buffer that is drained later, rather than updating remembered sets inline. |
|
||||||
|
| `ZTenuringThreshold` | `-1` | Age at which young objects are promoted. `-1` = adaptive. ZGC's answer to `MaxTenuringThreshold`. |
|
||||||
|
| `ZUseMediumPageSizeRange` | `true` | JDK 25: the medium page size is a **range** chosen at runtime, not a constant. This is why there is no `ZPageSizeMedium` product flag to read. |
|
||||||
|
| `ZStressFastMediumPageAllocation` | `false` | Test hook. |
|
||||||
|
| `ZIndexDistributorStrategy` | `0` | Internal work-distribution strategy for parallel phases. |
|
||||||
|
| `ZStatisticsInterval` | `10` (s) | Interval for `-Xlog:gc+stats`. |
|
||||||
|
|
||||||
|
## 4.3 G1 — the flags that still matter on JDK 25
|
||||||
|
|
||||||
|
| Flag | Default (2 GB heap) | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `-XX:+UseG1GC` | default collector on server-class machines | |
|
||||||
|
| `-XX:MaxGCPauseMillis` | `200` | A *goal*, not a guarantee. G1 sizes the young generation to try to hit it. Setting it very low (e.g. 5) makes G1 shrink young until it collects constantly — a classic self-inflicted throughput cliff. |
|
||||||
|
| `-XX:G1HeapRegionSize` | ergonomic, `1 MB` at 2 GB heap | Power of two, 1–32 MB, roughly heap/2048. **Determines the humongous threshold** (half a region). |
|
||||||
|
| `-XX:G1NewSizePercent` / `G1MaxNewSizePercent` | `5` / `60` | Bounds on young generation size. |
|
||||||
|
| `-XX:InitiatingHeapOccupancyPercent` | `45` | When to start a concurrent cycle. G1 adapts this by default (`-XX:-G1UseAdaptiveIHOP` to pin it). |
|
||||||
|
| `-XX:G1MixedGCLiveThresholdPercent` | `85` | Old regions above this liveness are not collected in mixed GCs. |
|
||||||
|
| `-XX:MaxTenuringThreshold` | `15` | Real, documented, product flag — unlike ZGC's. |
|
||||||
|
|
||||||
|
**The humongous threshold is the G1 flag most people should check and don't.** At a 2 GB heap the
|
||||||
|
region size is 1 MB, so anything over 512 KB is humongous: allocated straight into the old
|
||||||
|
generation, spanning whole regions, never given the cheap young-collection path. A service that
|
||||||
|
allocates 1 MB buffers is generating humongous objects on every request. Confirmed on this machine:
|
||||||
|
|
||||||
|
```console
|
||||||
|
G1HeapRegionSize : 1,048,576 bytes (1 MB)
|
||||||
|
=> G1 humongous threshold: objects larger than 524,288 bytes (512 KB) go straight to old gen
|
||||||
|
```
|
||||||
|
|
||||||
|
Raising `-XX:G1HeapRegionSize=8m` moves the threshold to 4 MB and can transform such a workload.
|
||||||
|
|
||||||
|
## 4.4 Two ergonomic surprises worth knowing
|
||||||
|
|
||||||
|
**`ZUncommit` silently turns itself off when `-Xms == -Xmx`.** Verified:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ java -XX:+UseZGC -Xms2g -Xmx2g ... PageSizeDemo
|
||||||
|
ZUncommit = false
|
||||||
|
```
|
||||||
|
|
||||||
|
but with a range (`-Xms512m -Xmx2g`) it is `true`. This is logical — you asked for a fixed heap, so
|
||||||
|
there is nothing to give back — but it surprises people who set a fixed heap *and* expect memory to
|
||||||
|
be returned to a container.
|
||||||
|
|
||||||
|
**`SoftMaxHeapSize` defaults to `-Xmx`, which means "no reserve".** Its default is ergonomic and
|
||||||
|
equal to max heap, so out of the box it does nothing. It only becomes useful when you set it.
|
||||||
|
|
||||||
|
## 4.5 A starting configuration
|
||||||
|
|
||||||
|
For a latency-sensitive JVM service on JDK 25, this is a defensible starting point — not because
|
||||||
|
these values are optimal, but because each one is there for a stated reason:
|
||||||
|
|
||||||
|
```
|
||||||
|
-XX:+UseZGC
|
||||||
|
-Xms8g -Xmx8g # fixed heap: no resize noise
|
||||||
|
-XX:SoftMaxHeapSize=6g # 25% reserve for allocation spikes
|
||||||
|
-XX:ZAllocationSpikeTolerance=3 # this service ramps fast
|
||||||
|
-Xlog:gc*,gc+alloc=debug:file=gc.log:time,uptime,level,tags:filecount=5,filesize=50m
|
||||||
|
-XX:StartFlightRecording=settings=profile,filename=app.jfr,maxsize=500m
|
||||||
|
```
|
||||||
|
|
||||||
|
plus a `.jfc` override setting `jdk.ZAllocationStall` threshold to `0 ms`
|
||||||
|
(see [03-allocation-stalls.md](03-allocation-stalls.md) §3.3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Next: [05-logging-and-jfr.md](05-logging-and-jfr.md).
|
||||||
137
docs/05-logging-and-jfr.md
Normal file
137
docs/05-logging-and-jfr.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# 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).
|
||||||
136
docs/06-methodology.md
Normal file
136
docs/06-methodology.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# 6. Methodology — how to not publish a meaningless GC benchmark
|
||||||
|
|
||||||
|
Most GC comparisons on the internet are wrong in one of a small number of specific, fixable ways.
|
||||||
|
This chapter lists them, because a benchmark's credibility is entirely a function of which of these
|
||||||
|
it avoided.
|
||||||
|
|
||||||
|
## 6.1 Coordinated omission
|
||||||
|
|
||||||
|
**The mistake.** A closed-loop harness — `while (true) { doRequest(); }` — stops issuing load exactly
|
||||||
|
when the system is least able to serve it. If a 100 ms pause freezes the application, the load
|
||||||
|
generator does not send requests during those 100 ms. It records one slow request per thread and
|
||||||
|
moves on. The pause was real; your users experienced it as 100 ms of queueing across ~2000 requests;
|
||||||
|
the benchmark reports it as a handful of samples.
|
||||||
|
|
||||||
|
**The fix.** Fix the arrival schedule up front and measure from *intended* arrival, not from when
|
||||||
|
the thread actually got around to the request.
|
||||||
|
|
||||||
|
```java
|
||||||
|
long intended = start + i * interArrivalNanos;
|
||||||
|
parkUntil(intended);
|
||||||
|
long actualStart = System.nanoTime();
|
||||||
|
handleRequest();
|
||||||
|
long done = System.nanoTime();
|
||||||
|
|
||||||
|
responseTime.record(done - intended); // honest
|
||||||
|
serviceTime.record(done - actualStart); // what a closed-loop harness reports
|
||||||
|
```
|
||||||
|
|
||||||
|
**How much it matters.** This repository records both and prints the ratio. From the 60-second run
|
||||||
|
on the reference machine:
|
||||||
|
|
||||||
|
| Collector | p99.9 response time | p99.9 service time | ratio |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ZGC | 1.437 ms | 0.143 ms | 10x |
|
||||||
|
| G1 | 95.169 ms | 0.033 ms | **2910x** |
|
||||||
|
|
||||||
|
A closed-loop benchmark of this exact workload would have concluded that G1's p99.9 is 0.033 ms —
|
||||||
|
*better* than ZGC's 0.143 ms — and published a chart showing G1 winning on tail latency. The honest
|
||||||
|
numbers say G1's p99.9 is 66x worse.
|
||||||
|
|
||||||
|
Note that the distortion is not uniform: it is far larger for the collector with larger pauses. So
|
||||||
|
coordinated omission does not merely add noise, it **systematically favours the collector you are
|
||||||
|
trying to evaluate against**.
|
||||||
|
|
||||||
|
## 6.2 Benchmarking the sleep instead of the system
|
||||||
|
|
||||||
|
The first version of this harness used `LockSupport.parkNanos(remaining)` between requests. On
|
||||||
|
Windows a park rounds up to roughly 1 ms, and the inter-arrival interval was 200 µs. Every park
|
||||||
|
overshot by several intervals; the thread then blasted through the backlog and parked again. The
|
||||||
|
harness reported a p50 of 0.590 ms for a request that takes 3 µs, and the shape of the distribution
|
||||||
|
looked plausibly like a GC effect.
|
||||||
|
|
||||||
|
The fix is to park only when the deadline is far away (3 ms here, comfortably beyond the timer
|
||||||
|
slack) and busy-spin the rest. It costs a core per worker thread, which is why the worker count
|
||||||
|
defaults to a *third* of the logical CPUs — see §6.4.
|
||||||
|
|
||||||
|
If your latency harness's p50 is much larger than the work it does, suspect your sleep before you
|
||||||
|
suspect the collector.
|
||||||
|
|
||||||
|
## 6.3 Comparing pause time against cycle time
|
||||||
|
|
||||||
|
Covered in detail in [05-logging-and-jfr.md](05-logging-and-jfr.md). In one line: ZGC's
|
||||||
|
`GarbageCollectorMXBean` "Cycles" beans and its `Concurrent ...` log lines are **not** freeze time,
|
||||||
|
and summing them against G1's pause numbers produces a comparison that is off by two orders of
|
||||||
|
magnitude in G1's favour.
|
||||||
|
|
||||||
|
## 6.4 Starving the collector of CPU
|
||||||
|
|
||||||
|
ZGC does concurrent work on its own threads. If the benchmark's load generator occupies every core —
|
||||||
|
which a busy-spinning open-loop harness will happily do — those threads cannot run, the collector
|
||||||
|
falls behind, and you measure a scheduling problem while believing you measured a GC.
|
||||||
|
|
||||||
|
This repository's harness therefore defaults to `availableProcessors() / 3` worker threads and states
|
||||||
|
so in its output. On the 12-thread reference machine that is 4 spinning workers, leaving 8 logical
|
||||||
|
CPUs for the JVM's own threads.
|
||||||
|
|
||||||
|
## 6.5 Letting the heap resize during measurement
|
||||||
|
|
||||||
|
`-Xms` ≠ `-Xmx` means the heap grows and shrinks during the run, and you are partly measuring the
|
||||||
|
resize policy. Every run in this repository uses `-Xms2g -Xmx2g`.
|
||||||
|
|
||||||
|
A side effect worth knowing: **`ZUncommit` automatically becomes `false` when `-Xms == -Xmx`**
|
||||||
|
(verified in [`results/05-pages-zgc.txt`](../results/05-pages-zgc.txt)). Fixing the heap therefore
|
||||||
|
also removes uncommit behaviour from the comparison. That is the right call for a benchmark and the
|
||||||
|
wrong call if uncommit is the thing you wanted to study.
|
||||||
|
|
||||||
|
## 6.6 Forgetting that JMH forks a fresh JVM
|
||||||
|
|
||||||
|
This one silently invalidates a large fraction of published GC microbenchmarks:
|
||||||
|
|
||||||
|
```console
|
||||||
|
# WRONG -- the fork does not inherit the outer JVM's GC flags
|
||||||
|
java -XX:+UseZGC -jar benchmarks.jar
|
||||||
|
|
||||||
|
# RIGHT
|
||||||
|
java -jar benchmarks.jar -jvmArgs "-XX:+UseZGC -Xms2g -Xmx2g"
|
||||||
|
```
|
||||||
|
|
||||||
|
JMH starts a separate JVM for each trial. Flags on the outer `java` command configure the *harness*,
|
||||||
|
not the forked JVM under measurement. Run it the wrong way twice and you get two runs of the default
|
||||||
|
collector, a pleasingly small difference between them, and a blog post.
|
||||||
|
|
||||||
|
Verify with `-XX:+PrintFlagsFinal` inside the fork, or simply read JMH's own `# VM options:` banner —
|
||||||
|
it prints the fork's arguments.
|
||||||
|
|
||||||
|
## 6.7 Measuring an empty heap
|
||||||
|
|
||||||
|
A collector with nothing to trace is fast. A benchmark that only allocates immediately-dead garbage
|
||||||
|
measures the TLAB allocation path and nothing else — and every collector has essentially the same
|
||||||
|
TLAB path.
|
||||||
|
|
||||||
|
The interesting variable is the **live set**: how much genuinely reachable data must be marked, and
|
||||||
|
how often it is mutated. This repository's harness holds ~585 MB live in a 2 GB heap and rewrites a
|
||||||
|
random slot on 20% of requests. `PromotionBench` parameterises the same idea directly through
|
||||||
|
`survivorDepth`.
|
||||||
|
|
||||||
|
## 6.8 Dead-code elimination
|
||||||
|
|
||||||
|
If the JIT can prove your allocation is unused, it deletes it, and you benchmark an empty loop. Use
|
||||||
|
JMH's `Blackhole` or return values, and — critically — **check `gc.alloc.rate.norm`** from
|
||||||
|
`-prof gc`. It reports bytes allocated per operation. If that number is not what the source code
|
||||||
|
implies, escape analysis removed something and the benchmark is measuring nothing.
|
||||||
|
|
||||||
|
## 6.9 One run is not a measurement
|
||||||
|
|
||||||
|
Everything in [`results/`](../results) is a single run on a single laptop, and is presented as an
|
||||||
|
illustration of *shape*, not as a benchmark result you should plan capacity from. Ryzen 5 5600U,
|
||||||
|
Windows 11, 15.3 GB RAM, JDK 25.0.3 — with browser tabs, an OS, and thermal limits. Server hardware,
|
||||||
|
Linux, and a quiet machine will produce different absolute numbers.
|
||||||
|
|
||||||
|
What generalises is the relationship: **G1 wins the median, ZGC wins the tail, and the gap widens as
|
||||||
|
the live set grows.** What does not generalise is any specific millisecond figure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Next: [07-corner-cases.md](07-corner-cases.md).
|
||||||
163
docs/07-corner-cases.md
Normal file
163
docs/07-corner-cases.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 7. Corner cases and things that bite
|
||||||
|
|
||||||
|
The blog post covers the headline concepts. This chapter is the long tail — the behaviours that do
|
||||||
|
not fit a narrative but do show up in production. Every claim here was verified on
|
||||||
|
`java 25.0.3+9-LTS-195`; raw dumps in [`results/`](../results).
|
||||||
|
|
||||||
|
## 7.1 ZGC turns off compressed oops. This is the biggest hidden cost.
|
||||||
|
|
||||||
|
On a heap under ~32 GB, HotSpot normally stores references as 32-bit offsets rather than 64-bit
|
||||||
|
pointers ("compressed oops"), roughly halving the space taken by reference fields. **ZGC cannot do
|
||||||
|
this**, because it needs the high bits of every pointer for the color. Verified:
|
||||||
|
|
||||||
|
```console
|
||||||
|
# ZGC, 2 GB heap
|
||||||
|
bool UseCompressedOops = false {product lp64_product} {ergonomic}
|
||||||
|
|
||||||
|
# G1, same 2 GB heap
|
||||||
|
bool UseCompressedOops = true {product lp64_product} {ergonomic}
|
||||||
|
```
|
||||||
|
|
||||||
|
Consequences, in rough order of how often they surprise people:
|
||||||
|
|
||||||
|
- **Every reference field costs 8 bytes instead of 4.** A `HashMap.Node` (hash, key, value, next) goes
|
||||||
|
from 32 bytes to 48. A reference-dense heap — many small objects, many pointers, think a large
|
||||||
|
in-memory graph, an ORM session cache, a JSON DOM — can grow **30–40%** just by switching
|
||||||
|
collector, with no application change.
|
||||||
|
- **Object header layout differs**, so heap-size estimates from a G1 heap dump do not transfer.
|
||||||
|
- **`-Xmx` is not comparable across collectors.** "We gave both 8 GB" is not a fair test if one of
|
||||||
|
them needs 11 GB to hold the same object graph. This repository uses a byte-array-heavy live set
|
||||||
|
partly for this reason: `byte[]` payloads are dominated by data, not references, which keeps the
|
||||||
|
comparison honest. A pointer-heavy live set would have handed ZGC a footprint penalty that has
|
||||||
|
nothing to do with pause behaviour.
|
||||||
|
- `UseCompressedClassPointers` stays `true` on both, so class pointers are unaffected.
|
||||||
|
|
||||||
|
If you are evaluating ZGC and your heap is full of small objects, measure footprint before you
|
||||||
|
measure latency.
|
||||||
|
|
||||||
|
## 7.2 ZGC large pages are never relocated
|
||||||
|
|
||||||
|
ZGC's Large pages hold exactly one object each and are **freed, not compacted**. A heap with many
|
||||||
|
long-lived multi-megabyte objects therefore fragments in a way ZGC structurally cannot fix. G1 has
|
||||||
|
the mirror problem with humongous regions, but G1 has a Full GC that *can* compact them (at
|
||||||
|
enormous cost).
|
||||||
|
|
||||||
|
Verified placement on a 2 GB heap — note the last line, which is the one that catches people:
|
||||||
|
|
||||||
|
```console
|
||||||
|
ZGC pages actually allocated while creating the objects above
|
||||||
|
page type count total bytes
|
||||||
|
Large 2 25,165,824
|
||||||
|
Medium 1 33,554,432
|
||||||
|
```
|
||||||
|
|
||||||
|
Two Large pages for the 4 MB and 16 MB allocations. **The 4 MB one is the surprise:** the documented
|
||||||
|
medium-object limit is 4 MB, but `new byte[4 * 1024 * 1024]` is 4 MB *plus a 16-byte array header*,
|
||||||
|
which is over the limit. It gets an 8 MB Large page to itself — a 2x footprint amplification caused
|
||||||
|
by sixteen bytes. If you pool buffers, pool them at `4 MB - 64` rather than exactly 4 MB.
|
||||||
|
|
||||||
|
## 7.3 G1's humongous threshold is lower than people think
|
||||||
|
|
||||||
|
At a 2 GB heap, `G1HeapRegionSize` is ergonomically 1 MB, so **anything over 512 KB is humongous**:
|
||||||
|
allocated straight into the old generation, spanning whole regions, never eligible for the cheap
|
||||||
|
young-collection path.
|
||||||
|
|
||||||
|
```console
|
||||||
|
G1HeapRegionSize : 1,048,576 bytes (1 MB)
|
||||||
|
=> G1 humongous threshold: objects larger than 524,288 bytes (512 KB) go straight to old gen
|
||||||
|
```
|
||||||
|
|
||||||
|
A service that reads 1 MB request bodies into a single `byte[]` is producing humongous garbage on
|
||||||
|
every request. `-XX:G1HeapRegionSize=8m` moves the threshold to 4 MB and can be transformative.
|
||||||
|
This is one of the few G1 flags that regularly delivers a large win, and it is invisible unless you
|
||||||
|
go looking.
|
||||||
|
|
||||||
|
## 7.4 Virtual thread stacks live on the heap
|
||||||
|
|
||||||
|
A mounted virtual thread runs on a carrier's stack, but an **unmounted** one has its stack copied
|
||||||
|
into the heap as `StackChunk` objects. So a service with 200,000 parked virtual threads is holding
|
||||||
|
200,000 medium-sized, medium-lived objects that the collector must trace.
|
||||||
|
|
||||||
|
This interacts badly with a naive reading of "virtual threads are cheap": they are cheap in
|
||||||
|
*scheduler* terms and not free in *GC* terms. Applications that move from a 200-thread pool to
|
||||||
|
200,000 virtual threads often see their live set and promotion rate change shape entirely, which is
|
||||||
|
exactly the regime where the ZGC-vs-G1 answer flips. If you have made that migration, re-run your GC
|
||||||
|
choice; the decision you made before it is stale.
|
||||||
|
|
||||||
|
## 7.5 `System.gc()` behaves differently
|
||||||
|
|
||||||
|
`ExplicitGCInvokesConcurrent` is `false` by default on **both** collectors here. Under G1 that means
|
||||||
|
an explicit `System.gc()` triggers a **Full GC** — a stop-the-world compaction. Under ZGC it
|
||||||
|
triggers a major cycle, most of which is still concurrent, so the blast radius is much smaller.
|
||||||
|
|
||||||
|
Libraries that call `System.gc()` (some direct-`ByteBuffer` cleanup paths, some test harnesses, some
|
||||||
|
profilers) are therefore far more damaging on G1. `-XX:+ExplicitGCInvokesConcurrent` is a cheap
|
||||||
|
mitigation on G1; `-XX:+DisableExplicitGC` is the blunt one.
|
||||||
|
|
||||||
|
Note that `StoreBarrierBench` in this repository calls `System.gc()` twice in `@Setup` — deliberately,
|
||||||
|
to force the array into the old generation before measurement, and outside the measured window.
|
||||||
|
|
||||||
|
## 7.6 Reference processing still stops the world briefly
|
||||||
|
|
||||||
|
Weak, soft, phantom references and finalizers are processed during specific phases. On ZGC this
|
||||||
|
happens under `Concurrent Process Non-Strong`, visible in the log:
|
||||||
|
|
||||||
|
```
|
||||||
|
Concurrent Process Non-Strong n=8 total= 5.635 ms max= 0.984 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Concurrent, therefore not freeze time — but a heap with millions of weak references still costs
|
||||||
|
real CPU and can extend cycle duration enough to trigger allocation stalls indirectly. `WeakHashMap`
|
||||||
|
at scale, and caches built on `SoftReference`, are the usual suspects. Soft references in particular
|
||||||
|
interact with `SoftRefLRUPolicyMSPerMB` and are cleared based on *free heap*, which behaves very
|
||||||
|
differently when `SoftMaxHeapSize` is set.
|
||||||
|
|
||||||
|
## 7.7 `ZUncommit` disables itself on a fixed heap
|
||||||
|
|
||||||
|
```console
|
||||||
|
# -Xms2g -Xmx2g
|
||||||
|
ZUncommit = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Logical — there is nothing to give back — but it surprises teams who set a fixed heap for
|
||||||
|
predictability *and* expect memory returned to the container. If you want uncommit, you must give
|
||||||
|
`-Xms` and `-Xmx` different values, and tune `ZUncommitDelay` (default 300 s) down if you want it to
|
||||||
|
happen promptly.
|
||||||
|
|
||||||
|
## 7.8 GC thread counts differ by default
|
||||||
|
|
||||||
|
```console
|
||||||
|
ZGC : ParallelGCThreads = 8, ConcGCThreads = 3
|
||||||
|
G1 : ParallelGCThreads = 10, ConcGCThreads = 3
|
||||||
|
```
|
||||||
|
|
||||||
|
On the same 12-thread machine. If you pin GC threads for consistency across a fleet, you are
|
||||||
|
overriding two different ergonomic decisions with one number, and one of the collectors will be
|
||||||
|
mis-sized. Prefer leaving them ergonomic unless you are running in a CPU-limited container, where
|
||||||
|
you should set them explicitly because the JVM's view of `availableProcessors()` may not match the
|
||||||
|
cgroup quota.
|
||||||
|
|
||||||
|
## 7.9 Container limits
|
||||||
|
|
||||||
|
Both collectors respect cgroup limits, but the failure modes differ:
|
||||||
|
|
||||||
|
- ZGC's higher footprint (see §7.1) plus its need for headroom means a container sized for G1 will
|
||||||
|
push ZGC into allocation stalls. Budget more memory, not the same memory.
|
||||||
|
- `-XX:MaxRAMPercentage` is the right knob in containers; `-Xmx` hard-codes a number that will be
|
||||||
|
wrong after the next resize.
|
||||||
|
- ZGC reserves a large *virtual* address range. Tools that alarm on virtual size rather than RSS
|
||||||
|
will report frightening numbers. Alarm on RSS.
|
||||||
|
|
||||||
|
## 7.10 Things that are the same and often assumed not to be
|
||||||
|
|
||||||
|
- **String deduplication** works on ZGC (since JDK 18), not just G1. `-XX:+UseStringDeduplication`
|
||||||
|
is off by default on both.
|
||||||
|
- **Class unloading** happens on both, during major/concurrent cycles.
|
||||||
|
- **TLABs** work identically; the fast allocation path is a pointer bump on both.
|
||||||
|
- **Heap dumps** (`jcmd GC.heap_dump`) work the same and are stop-the-world on both.
|
||||||
|
- **`-XX:+AlwaysPreTouch`** is `false` by default on both, and is worth turning on for both if you
|
||||||
|
care about first-minute latency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Next: [08-choosing.md](08-choosing.md).
|
||||||
97
docs/08-choosing.md
Normal file
97
docs/08-choosing.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 8. Choosing a collector on JDK 25
|
||||||
|
|
||||||
|
## 8.1 The short version
|
||||||
|
|
||||||
|
**G1 is still the right default.** It is the default for a reason: it is balanced, it needs no
|
||||||
|
tuning to be adequate, and it wins the median. Switch to ZGC when you have a *stated* tail-latency
|
||||||
|
requirement that G1 is measurably missing — not because ZGC is newer.
|
||||||
|
|
||||||
|
## 8.2 What the measurements actually say
|
||||||
|
|
||||||
|
From the 60-second run in this repository (Ryzen 5 5600U, Windows 11, JDK 25.0.3, 2 GB heap,
|
||||||
|
~585 MB live, 20,000 req/s, ~328 MB/s allocation rate):
|
||||||
|
|
||||||
|
| Metric | ZGC | G1 | Winner |
|
||||||
|
|---|---|---|---|
|
||||||
|
| p50 response time | 0.006 ms | 0.005 ms | G1, barely |
|
||||||
|
| p90 | 0.009 ms | 0.009 ms | tie |
|
||||||
|
| p99 | 0.029 ms | 8.042 ms | **ZGC, 277x** |
|
||||||
|
| p99.9 | 1.437 ms | 95.169 ms | **ZGC, 66x** |
|
||||||
|
| p99.99 | 23.675 ms | 126.949 ms | **ZGC, 5.4x** |
|
||||||
|
| max | 32.178 ms | 133.018 ms | ZGC |
|
||||||
|
| mean | 0.016 ms | 0.388 ms | ZGC |
|
||||||
|
| total stop-the-world time | **1.503 ms** | **1,139.6 ms** | **ZGC, 758x** |
|
||||||
|
| longest single pause | 0.054 ms | 104.449 ms | ZGC |
|
||||||
|
| Full GCs / evacuation failures | 0 / 0 | 4 / 16 | ZGC |
|
||||||
|
|
||||||
|
Note the shape: the two collectors are **indistinguishable up to p90**. Everything ZGC buys you is
|
||||||
|
in the tail. If your SLO is a mean or a p95, this entire table is telling you to stay on G1 and
|
||||||
|
spend the effort elsewhere.
|
||||||
|
|
||||||
|
Note also that ZGC is not magic: its own p99.99 is 23.7 ms. Those are allocation stalls (the GC log
|
||||||
|
for that run contains 40 of them), not pauses. ZGC moved the latency source, it did not delete it.
|
||||||
|
|
||||||
|
## 8.3 A decision procedure
|
||||||
|
|
||||||
|
Work through it in order. Stop at the first "yes".
|
||||||
|
|
||||||
|
1. **Is GC actually your problem?** Get a JFR recording and look at `jdk.GCPhasePause` and
|
||||||
|
`jdk.ZAllocationStall` against your request latency. Most "GC problems" are lock contention,
|
||||||
|
downstream calls, or connection-pool starvation. Changing collector will not fix those and will
|
||||||
|
cost you a week.
|
||||||
|
2. **Is your heap small (< 4 GB) and your SLO a mean or p95?** Stay on G1. ZGC's advantages need a
|
||||||
|
tail requirement and a heap large enough that marking cost matters.
|
||||||
|
3. **Do you have a hard tail-latency SLO — p99 or p99.9 in single-digit or low-double-digit
|
||||||
|
milliseconds — that G1 is missing?** This is the case ZGC was built for. Go to step 5.
|
||||||
|
4. **Is your heap very large (> 32 GB) with a large live set?** G1's pause time scales with live
|
||||||
|
data in the collection set; ZGC's does not. ZGC becomes increasingly favourable as the heap
|
||||||
|
grows. Go to step 5.
|
||||||
|
5. **Can you afford the footprint?** ZGC needs headroom (§8.4) *and* loses compressed oops
|
||||||
|
([07-corner-cases.md](07-corner-cases.md) §7.1). Budget 30–50% more memory than the equivalent
|
||||||
|
G1 deployment, more if your heap is reference-dense. If you cannot, ZGC will stall and you will
|
||||||
|
conclude — wrongly — that ZGC is bad.
|
||||||
|
6. **Is your workload throughput-bound with a hot pointer-chasing inner loop?** ZGC's load barrier
|
||||||
|
is a real, permanent tax on reference loads. Batch jobs, analytics, and anything that walks large
|
||||||
|
object graphs should measure before switching. Consider Parallel GC instead — for a batch job
|
||||||
|
with no latency requirement, Parallel still beats both on raw throughput.
|
||||||
|
|
||||||
|
## 8.4 Sizing ZGC
|
||||||
|
|
||||||
|
The single most common ZGC failure is a heap sized as if it were a G1 heap.
|
||||||
|
|
||||||
|
- Start at **1.5x** the `-Xmx` you used for G1, then measure.
|
||||||
|
- Set `SoftMaxHeapSize` to roughly **75%** of `-Xmx` so there is a reserve for allocation spikes.
|
||||||
|
- Watch `jdk.ZAllocationStall` (threshold `0 ms`), not pause time. If stalls are non-zero under
|
||||||
|
normal load, you are under-provisioned.
|
||||||
|
- If stalls are concentrated on Large pages, the fix is in your application's allocation sizes, not
|
||||||
|
in the heap size.
|
||||||
|
|
||||||
|
## 8.5 When the answer is neither
|
||||||
|
|
||||||
|
- **Parallel GC** (`-XX:+UseParallelGC`) — batch jobs, ETL, anything where wall-clock throughput is
|
||||||
|
the only metric and a multi-second pause is irrelevant. Still the throughput champion.
|
||||||
|
- **Serial GC** (`-XX:+UseSerialGC`) — small containers, single-core, short-lived processes. Lowest
|
||||||
|
overhead, smallest footprint, fastest start-up.
|
||||||
|
- **Shenandoah** (`-XX:+UseShenandoahGC`) — the other concurrent-compacting collector. Broadly
|
||||||
|
similar goals to ZGC with a different barrier design (Brooks-style forwarding rather than colored
|
||||||
|
pointers); it keeps compressed oops, which makes it interesting precisely where §7.1 hurts.
|
||||||
|
Worth benchmarking as a third option if footprint is your constraint.
|
||||||
|
|
||||||
|
## 8.6 The honest summary
|
||||||
|
|
||||||
|
ZGC does not make garbage collection free. It converts a **rare, large, correlated** cost — a
|
||||||
|
stop-the-world pause that hits every thread at once — into a **constant, small, uncorrelated** cost:
|
||||||
|
barrier work on every reference access, plus a memory footprint premium.
|
||||||
|
|
||||||
|
For a request-serving system with a tail-latency SLO, that trade is close to unambiguously good, and
|
||||||
|
JDK 25 is the first LTS where you get it without an opt-in flag or a non-generational fallback to
|
||||||
|
worry about.
|
||||||
|
|
||||||
|
For a batch job, it is a straightforward loss.
|
||||||
|
|
||||||
|
Most systems are somewhere in between, which is why the only defensible answer is to run
|
||||||
|
[`scripts/run-all.ps1`](../scripts/run-all.ps1) against a workload shaped like yours.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Back to the [README](../README.md).
|
||||||
74
env/Env.java
vendored
Normal file
74
env/Env.java
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Env.java -- prints everything a GC measurement needs in order to be reproducible.
|
||||||
|
//
|
||||||
|
// Run it (JDK 25 single-file source launch, no compile step needed):
|
||||||
|
//
|
||||||
|
// java env/Env.java
|
||||||
|
// java -XX:+UseZGC -Xms2g -Xmx2g env/Env.java
|
||||||
|
// java -XX:+UseG1GC -Xms2g -Xmx2g env/Env.java
|
||||||
|
//
|
||||||
|
// Why this file exists: a GC benchmark without the environment printed next to it
|
||||||
|
// is an anecdote. Heap size, CPU count, GC selection and the *ergonomic* values the
|
||||||
|
// JVM picked for you (SoftMaxHeapSize, region size, GC thread counts) all move the
|
||||||
|
// numbers more than the code under test does.
|
||||||
|
|
||||||
|
import java.lang.management.GarbageCollectorMXBean;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.MemoryPoolMXBean;
|
||||||
|
import java.lang.management.RuntimeMXBean;
|
||||||
|
|
||||||
|
public final class Env {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
|
||||||
|
|
||||||
|
System.out.println("=== Runtime ===");
|
||||||
|
System.out.printf("java.version : %s%n", System.getProperty("java.version"));
|
||||||
|
System.out.printf("java.vm.name : %s%n", System.getProperty("java.vm.name"));
|
||||||
|
System.out.printf("java.vm.version : %s%n", System.getProperty("java.vm.version"));
|
||||||
|
System.out.printf("java.vendor : %s%n", System.getProperty("java.vendor"));
|
||||||
|
System.out.printf("os.name / os.arch : %s / %s%n",
|
||||||
|
System.getProperty("os.name"), System.getProperty("os.arch"));
|
||||||
|
System.out.printf("availableProcessors : %d%n", Runtime.getRuntime().availableProcessors());
|
||||||
|
System.out.printf("maxMemory (-Xmx) : %s%n", mb(Runtime.getRuntime().maxMemory()));
|
||||||
|
System.out.printf("totalMemory (now) : %s%n", mb(Runtime.getRuntime().totalMemory()));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== Collector in use ===");
|
||||||
|
// On JDK 25 the ZGC bean names are "ZGC Major Cycles" / "ZGC Minor Cycles" (and the
|
||||||
|
// matching "... Pauses" beans). The presence of BOTH a Minor and a Major bean is the
|
||||||
|
// cheapest programmatic proof that you are running *generational* ZGC.
|
||||||
|
boolean sawMinor = false, sawMajor = false;
|
||||||
|
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||||
|
System.out.printf(" bean: %-24s count=%-6d timeMs=%d%n",
|
||||||
|
gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
|
||||||
|
String n = gc.getName().toLowerCase();
|
||||||
|
if (n.contains("minor") || n.contains("young")) sawMinor = true;
|
||||||
|
if (n.contains("major") || n.contains("old") || n.contains("full")) sawMajor = true;
|
||||||
|
}
|
||||||
|
System.out.printf(" generational? : %s%n",
|
||||||
|
(sawMinor && sawMajor) ? "yes (separate young + old cycles reported)" : "no / single cycle");
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== Memory pools ===");
|
||||||
|
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
|
||||||
|
System.out.printf(" %-28s type=%-10s max=%s%n",
|
||||||
|
pool.getName(), pool.getType(), mb(pool.getUsage().getMax()));
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== JVM arguments actually in effect ===");
|
||||||
|
if (runtime.getInputArguments().isEmpty()) {
|
||||||
|
System.out.println(" (none -- everything below is ergonomic)");
|
||||||
|
}
|
||||||
|
runtime.getInputArguments().forEach(a -> System.out.println(" " + a));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("Tip: dump every ergonomic value the JVM chose with");
|
||||||
|
System.out.println(" java -XX:+UseZGC -Xms2g -Xmx2g -XX:+PrintFlagsFinal -version");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String mb(long bytes) {
|
||||||
|
if (bytes < 0) return "unbounded";
|
||||||
|
return String.format("%,d bytes (%.0f MB)", bytes, bytes / 1024.0 / 1024.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
internals/src/com/ankurm/zgc/internals/PageSizeDemo.java
Normal file
222
internals/src/com/ankurm/zgc/internals/PageSizeDemo.java
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package com.ankurm.zgc.internals;
|
||||||
|
|
||||||
|
import com.sun.management.HotSpotDiagnosticMXBean;
|
||||||
|
import jdk.jfr.Recording;
|
||||||
|
import jdk.jfr.consumer.RecordedEvent;
|
||||||
|
import jdk.jfr.consumer.RecordingFile;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where does a large object actually go?
|
||||||
|
*
|
||||||
|
* <p>Both collectors chop the heap into fixed units, and both have a special path for objects too
|
||||||
|
* big for one unit -- but the thresholds, the names and the consequences are completely different,
|
||||||
|
* and almost nothing written about ZGC explains the ZGC half.
|
||||||
|
*
|
||||||
|
* <h2>G1: regions and humongous objects</h2>
|
||||||
|
* G1 divides the heap into equal regions ({@code G1HeapRegionSize}, chosen ergonomically as roughly
|
||||||
|
* heap/2048, clamped to a power of two between 1 MB and 32 MB). An object larger than <b>half a
|
||||||
|
* region</b> is <i>humongous</i>: it is allocated directly into the old generation, in a run of
|
||||||
|
* contiguous regions, and it never gets the cheap young-collection treatment. On a 2 GB heap the
|
||||||
|
* region size is typically 1 MB, so an ordinary 600 KB byte array is already humongous. Humongous
|
||||||
|
* allocation is a well known source of surprise fragmentation and premature concurrent cycles.
|
||||||
|
*
|
||||||
|
* <h2>ZGC: three page classes</h2>
|
||||||
|
* ZGC uses <i>pages</i> in three size classes rather than one region size:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Small</b> -- 2 MB pages, for objects up to 1/8 of the page (256 KB).</li>
|
||||||
|
* <li><b>Medium</b> -- 32 MB by default, for objects up to 4 MB. Since JDK 23 the medium page
|
||||||
|
* size is chosen ergonomically from the heap size rather than being hard-wired, which is why
|
||||||
|
* {@code ZPageSizeMedium} may read differently on a small heap than on a large one.</li>
|
||||||
|
* <li><b>Large</b> -- one page per object, sized to the object. Despite the name these are the
|
||||||
|
* pages ZGC <em>cannot</em> relocate cheaply; a large page is not compacted, it is simply
|
||||||
|
* freed when the object dies.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>The practical consequence: on G1 a moderately large array is a special case at ~512 KB; on ZGC
|
||||||
|
* it stays ordinary until 256 KB, becomes "medium" up to 4 MB, and only then becomes a page of its
|
||||||
|
* own. Applications that allocate many 1-4 MB buffers behave very differently on the two.
|
||||||
|
*
|
||||||
|
* <h2>How this program shows it</h2>
|
||||||
|
* It reads {@code G1HeapRegionSize} / {@code ZPageSizeMedium} straight out of the running VM via
|
||||||
|
* {@link HotSpotDiagnosticMXBean}, prints the derived thresholds, then allocates one object of each
|
||||||
|
* of several sizes while a JFR recording captures {@code jdk.ZPageAllocation}. Reading those events
|
||||||
|
* back shows, empirically, which page class each allocation landed in -- not from documentation,
|
||||||
|
* from the running VM.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* java -XX:+UseZGC -Xms2g -Xmx2g -cp out com.ankurm.zgc.internals.PageSizeDemo
|
||||||
|
* java -XX:+UseG1GC -Xms2g -Xmx2g -cp out com.ankurm.zgc.internals.PageSizeDemo
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public final class PageSizeDemo {
|
||||||
|
|
||||||
|
/** Sizes chosen to straddle every threshold discussed above. */
|
||||||
|
private static final int[] SIZES = {
|
||||||
|
64 * 1024, // 64 KB -- small on both
|
||||||
|
256 * 1024, // 256 KB -- exactly ZGC's small-object limit
|
||||||
|
512 * 1024, // 512 KB -- humongous on G1 with a 1 MB region
|
||||||
|
2 * 1024 * 1024, // 2 MB -- ZGC medium
|
||||||
|
4 * 1024 * 1024, // 4 MB -- ZGC's medium-object limit
|
||||||
|
16 * 1024 * 1024, // 16 MB -- ZGC large, multi-region humongous on G1
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Object[] keepAlive;
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
HotSpotDiagnosticMXBean diag = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||||
|
|
||||||
|
System.out.println("=".repeat(74));
|
||||||
|
System.out.println("Page / region geometry as reported by THIS JVM");
|
||||||
|
System.out.println("=".repeat(74));
|
||||||
|
System.out.printf("max heap : %,d MB%n", Runtime.getRuntime().maxMemory() / 1024 / 1024);
|
||||||
|
|
||||||
|
String regionSize = flag(diag, "G1HeapRegionSize");
|
||||||
|
boolean g1 = "true".equals(flag(diag, "UseG1GC"));
|
||||||
|
boolean zgc = "true".equals(flag(diag, "UseZGC"));
|
||||||
|
|
||||||
|
System.out.printf("UseG1GC / UseZGC : %s / %s%n", g1, zgc);
|
||||||
|
System.out.printf("G1HeapRegionSize : %s%n", human(regionSize));
|
||||||
|
|
||||||
|
if (g1 && isNumeric(regionSize)) {
|
||||||
|
long region = Long.parseLong(regionSize);
|
||||||
|
System.out.printf("=> G1 humongous threshold: objects larger than %s go straight to old gen%n",
|
||||||
|
human(String.valueOf(region / 2)));
|
||||||
|
}
|
||||||
|
if (zgc) {
|
||||||
|
System.out.println("=> ZGC small pages are 2 MB; objects up to 1/8 of a page (256 KB) use them.");
|
||||||
|
System.out.println(" Larger objects use a medium page, and anything bigger gets a Large page");
|
||||||
|
System.out.println(" of its own -- which ZGC frees rather than relocates.");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("ZGC internals flags visible in this JVM");
|
||||||
|
// These are all {diagnostic} rather than {product}, which is why they appear in no
|
||||||
|
// tuning guide -- but they name the mechanisms that matter.
|
||||||
|
for (String name : new String[]{
|
||||||
|
"ZUseMediumPageSizeRange", // JDK 25: medium page size is a RANGE, not a constant
|
||||||
|
"ZBufferStoreBarriers", // the store barrier added by generational ZGC, buffered
|
||||||
|
"ZTenuringThreshold", // -1 == adaptive; ZGC ages objects like any generational GC
|
||||||
|
"ZIndexDistributorStrategy",
|
||||||
|
"ZStatisticsInterval",
|
||||||
|
"ZFragmentationLimit",
|
||||||
|
"ZCollectionIntervalMinor",
|
||||||
|
"ZCollectionIntervalMajor",
|
||||||
|
"ZAllocationSpikeTolerance",
|
||||||
|
"ZUncommit",
|
||||||
|
"ZUncommitDelay",
|
||||||
|
"SoftMaxHeapSize"}) {
|
||||||
|
String v = flag(diag, name);
|
||||||
|
if (!v.startsWith("n/a")) {
|
||||||
|
System.out.printf(" %-28s = %s%n", name, name.contains("Size") ? human(v) : v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
Path jfr = Path.of("page-allocations.jfr");
|
||||||
|
Map<Integer, String> observed = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
try (Recording recording = new Recording()) {
|
||||||
|
try {
|
||||||
|
recording.enable("jdk.ZPageAllocation").withoutThreshold();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
System.out.println("(jdk.ZPageAllocation unavailable -- not running ZGC)");
|
||||||
|
}
|
||||||
|
recording.setToDisk(true);
|
||||||
|
recording.setDestination(jfr);
|
||||||
|
recording.start();
|
||||||
|
|
||||||
|
keepAlive = new Object[SIZES.length];
|
||||||
|
for (int i = 0; i < SIZES.length; i++) {
|
||||||
|
keepAlive[i] = new byte[SIZES[i]];
|
||||||
|
((byte[]) keepAlive[i])[0] = 1;
|
||||||
|
}
|
||||||
|
recording.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(jfr)) {
|
||||||
|
Map<String, long[]> byType = new TreeMap<>(); // pageType -> {count, totalBytes}
|
||||||
|
try (RecordingFile file = new RecordingFile(jfr)) {
|
||||||
|
while (file.hasMoreEvents()) {
|
||||||
|
RecordedEvent e = file.readEvent();
|
||||||
|
if (!e.getEventType().getName().equals("jdk.ZPageAllocation")) continue;
|
||||||
|
// NOTE: RecordedEvent.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 blows up at runtime
|
||||||
|
// with ClassCastException: String cannot be cast to [C. Bind to Object first.
|
||||||
|
Object typeValue = e.hasField("type") ? e.getValue("type") : null;
|
||||||
|
Object sizeValue = e.hasField("size") ? e.getValue("size") : null;
|
||||||
|
String type = typeValue == null ? "?" : typeValue.toString();
|
||||||
|
long size = sizeValue instanceof Number n ? n.longValue() : 0L;
|
||||||
|
long[] agg = byType.computeIfAbsent(type, k -> new long[2]);
|
||||||
|
agg[0]++;
|
||||||
|
agg[1] += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (byType.isEmpty()) {
|
||||||
|
System.out.println("No jdk.ZPageAllocation events (expected under G1).");
|
||||||
|
} else {
|
||||||
|
System.out.println("ZGC pages actually allocated while creating the objects above");
|
||||||
|
System.out.printf(" %-10s %-10s %s%n", "page type", "count", "total bytes");
|
||||||
|
byType.forEach((type, agg) ->
|
||||||
|
System.out.printf(" %-10s %-10d %,d%n", type, agg[0], agg[1]));
|
||||||
|
}
|
||||||
|
Files.deleteIfExists(jfr);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("Objects allocated in this run");
|
||||||
|
for (int size : SIZES) {
|
||||||
|
System.out.printf(" %-24s %s%n", human(String.valueOf(size)), classify(size, regionSize, g1));
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("checksum: " + keepAlive.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predicts the placement. For ZGC this is only a prediction -- the authoritative answer is the
|
||||||
|
* {@code jdk.ZPageAllocation} table printed above, which comes from the running collector.
|
||||||
|
*/
|
||||||
|
private static String classify(int size, String regionSize, boolean g1) {
|
||||||
|
if (g1) {
|
||||||
|
if (!isNumeric(regionSize)) return "(unknown region size)";
|
||||||
|
long half = Long.parseLong(regionSize) / 2;
|
||||||
|
return size > half
|
||||||
|
? "HUMONGOUS on G1 -- allocated directly in old gen, spans whole regions"
|
||||||
|
: "ordinary G1 allocation in an eden region";
|
||||||
|
}
|
||||||
|
// The limits apply to the *object*, header included -- not to the array length you asked
|
||||||
|
// for. A new byte[4*1024*1024] is 4 MB + 16 bytes of header, which is over the medium-object
|
||||||
|
// limit, so it lands on a Large page. Exactly the sort of off-by-a-header that makes a
|
||||||
|
// capacity plan wrong by 8 MB per buffer. The JFR table above is the ground truth.
|
||||||
|
long objectBytes = size + 16L;
|
||||||
|
if (objectBytes <= 256 * 1024) return "expect ZGC Small page (2 MB, shared with other objects)";
|
||||||
|
if (objectBytes <= 4L * 1024 * 1024) return "expect ZGC Medium page (shared; size is a range in JDK 25)";
|
||||||
|
return "expect ZGC Large page -- one page for this object, freed not relocated";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String flag(HotSpotDiagnosticMXBean diag, String name) {
|
||||||
|
try {
|
||||||
|
return diag.getVMOption(name).getValue();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return "n/a on this collector";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNumeric(String s) {
|
||||||
|
return s != null && s.chars().allMatch(Character::isDigit) && !s.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String human(String bytes) {
|
||||||
|
if (!isNumeric(bytes)) return bytes;
|
||||||
|
long b = Long.parseLong(bytes);
|
||||||
|
if (b == 0) return "0 (ergonomic / not applicable)";
|
||||||
|
if (b >= 1024 * 1024) return "%,d bytes (%d MB)".formatted(b, b / 1024 / 1024);
|
||||||
|
return "%,d bytes (%d KB)".formatted(b, b / 1024);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
jmh/pom.xml
Normal file
93
jmh/pom.xml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
JMH module for the ZGC-vs-G1 comparison.
|
||||||
|
|
||||||
|
Build: mvn -q clean package
|
||||||
|
Run: java -XX:+UseZGC -Xms2g -Xmx2g -jar target/benchmarks.jar
|
||||||
|
java -XX:+UseG1GC -Xms2g -Xmx2g -jar target/benchmarks.jar
|
||||||
|
|
||||||
|
Note that the GC flags go on the *outer* java command only if you also pass -jvmArgsAppend, or
|
||||||
|
JMH will fork a fresh JVM without them. The run scripts in ../scripts do this correctly; see
|
||||||
|
the README for the explanation, it is a classic way to publish a meaningless GC benchmark.
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.ankurm.zgc</groupId>
|
||||||
|
<artifactId>zgc-jmh</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>ZGC vs G1 JMH benchmarks (JDK 25)</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.compiler.release>25</maven.compiler.release>
|
||||||
|
<jmh.version>1.37</jmh.version>
|
||||||
|
<uberjar.name>benchmarks</uberjar.name>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-core</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>${maven.compiler.release}</release>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals><goal>shade</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<finalName>${uberjar.name}</finalName>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
70
jmh/src/main/java/com/ankurm/zgc/jmh/AllocationBench.java
Normal file
70
jmh/src/main/java/com/ankurm/zgc/jmh/AllocationBench.java
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package com.ankurm.zgc.jmh;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.*;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How fast can the application allocate short-lived objects?
|
||||||
|
*
|
||||||
|
* <p>This is the benchmark everyone writes first, and on its own it is close to useless -- but it
|
||||||
|
* is useful as a <em>baseline</em>, because it isolates the one thing that is almost purely a
|
||||||
|
* function of the allocation path rather than of the marking or relocation machinery.
|
||||||
|
*
|
||||||
|
* <p>What it actually measures:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>The thread-local allocation buffer (TLAB) fast path, which is a pointer bump plus a bounds
|
||||||
|
* check on every collector. G1 and ZGC use the same idea here, so raw allocation of tiny
|
||||||
|
* objects tends to be a near-tie.</li>
|
||||||
|
* <li>The <em>size</em> of the young generation, indirectly: the more often the allocator has to
|
||||||
|
* refill a TLAB from a fresh region/page, the more the collector's page-allocation path
|
||||||
|
* shows up.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Run it with {@code -prof gc} and read the {@code gc.alloc.rate.norm} column -- it reports
|
||||||
|
* bytes allocated per operation. If that number is not exactly what you expect from the source
|
||||||
|
* code, escape analysis removed an allocation and you are benchmarking nothing:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* java -jar target/benchmarks.jar AllocationBench -prof gc -jvmArgs "-XX:+UseZGC -Xms2g -Xmx2g"
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
@Warmup(iterations = 3, time = 3)
|
||||||
|
@Measurement(iterations = 5, time = 3)
|
||||||
|
@Fork(1)
|
||||||
|
public class AllocationBench {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object sizes chosen to straddle the boundaries that matter:
|
||||||
|
* 64 B is a typical small POJO/array, 1 KB is a request buffer, 32 KB is large enough that
|
||||||
|
* on G1 it approaches the humongous threshold for small region sizes and on ZGC it stops
|
||||||
|
* fitting neatly into the small-page allocation path.
|
||||||
|
*/
|
||||||
|
@Param({"64", "1024", "32768"})
|
||||||
|
public int size;
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public byte[] allocate() {
|
||||||
|
// Returned (not consumed by a Blackhole) so JMH's return-value sink keeps it alive just
|
||||||
|
// long enough to prevent scalar replacement, without adding a second store to the loop.
|
||||||
|
byte[] b = new byte[size];
|
||||||
|
b[0] = 1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same allocation, but immediately dead. Comparing the two shows how much of the cost is
|
||||||
|
* the allocation itself versus keeping the reference alive for one more instruction.
|
||||||
|
*/
|
||||||
|
@Benchmark
|
||||||
|
public void allocateAndDrop(Blackhole bh) {
|
||||||
|
byte[] b = new byte[size];
|
||||||
|
b[0] = 1;
|
||||||
|
bh.consume(b[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
jmh/src/main/java/com/ankurm/zgc/jmh/LoadBarrierBench.java
Normal file
120
jmh/src/main/java/com/ankurm/zgc/jmh/LoadBarrierBench.java
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package com.ankurm.zgc.jmh;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.*;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Isolates the cost of ZGC's <b>load barrier</b>.
|
||||||
|
*
|
||||||
|
* <p>This is the benchmark that explains ZGC's throughput deficit, and almost nobody publishes it.
|
||||||
|
*
|
||||||
|
* <h2>What a load barrier is</h2>
|
||||||
|
* ZGC stores metadata in unused high bits of every heap pointer ("colored pointers"). Whenever your
|
||||||
|
* code reads a reference field, the JIT emits a few extra instructions that check those bits and,
|
||||||
|
* if the pointer is stale (its object has been relocated, or has not been marked yet in this
|
||||||
|
* cycle), fix it up before your code ever sees it. That fix-up is what lets ZGC relocate objects
|
||||||
|
* while the application keeps running -- there is no stop-the-world phase in which pointers are
|
||||||
|
* corrected, because every read corrects its own pointer lazily.
|
||||||
|
*
|
||||||
|
* <p>The price is that <em>every reference load</em> costs more. Not primitive loads -- {@code
|
||||||
|
* int[]} and {@code long} fields are untouched -- only loads of references. G1 has no read barrier
|
||||||
|
* at all, so on a workload that is dominated by chasing pointers through a large object graph, G1
|
||||||
|
* has a structural advantage.
|
||||||
|
*
|
||||||
|
* <h2>How this benchmark separates the two</h2>
|
||||||
|
* {@link #chaseReferences()} walks a linked structure: every step is a reference load and therefore
|
||||||
|
* a barrier. {@link #sumPrimitives()} walks an {@code int[]} of identical length: same memory
|
||||||
|
* traffic, same cache behaviour, zero reference loads and therefore zero barriers. The difference
|
||||||
|
* between the two, measured under ZGC and again under G1, is as close to "the barrier tax" as you
|
||||||
|
* can get without reading assembly.
|
||||||
|
*
|
||||||
|
* <p>Both variants are also affected by whether a GC cycle is in progress -- ZGC's barrier is
|
||||||
|
* cheapest when the heap is "clean" and does more work during marking and relocation. Running with
|
||||||
|
* {@code -XX:ZCollectionInterval=1} forces cycles during the measurement and shows the worst case.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* java -jar target/benchmarks.jar LoadBarrierBench -jvmArgs "-XX:+UseZGC -Xms2g -Xmx2g"
|
||||||
|
* java -jar target/benchmarks.jar LoadBarrierBench -jvmArgs "-XX:+UseG1GC -Xms2g -Xmx2g"
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@BenchmarkMode(Mode.AverageTime)
|
||||||
|
@OutputTimeUnit(TimeUnit.MICROSECONDS)
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
@Warmup(iterations = 3, time = 3)
|
||||||
|
@Measurement(iterations = 5, time = 3)
|
||||||
|
@Fork(1)
|
||||||
|
public class LoadBarrierBench {
|
||||||
|
|
||||||
|
/** Node count. 1<<20 nodes is ~32 MB of nodes -- comfortably beyond L3 on most desktops. */
|
||||||
|
@Param({"1048576"})
|
||||||
|
public int nodes;
|
||||||
|
|
||||||
|
static final class Node {
|
||||||
|
Node next; // reference field -- a load barrier fires on every read of this
|
||||||
|
int value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node head;
|
||||||
|
private int[] primitives;
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void setup() {
|
||||||
|
// Build the chain in a shuffled order so that pointer-chasing genuinely misses cache
|
||||||
|
// rather than walking linearly through memory the prefetcher can predict. A benchmark
|
||||||
|
// whose "random" walk is actually sequential measures the prefetcher, not the barrier.
|
||||||
|
Node[] all = new Node[nodes];
|
||||||
|
for (int i = 0; i < nodes; i++) {
|
||||||
|
all[i] = new Node();
|
||||||
|
all[i].value = i;
|
||||||
|
}
|
||||||
|
int[] order = new int[nodes];
|
||||||
|
for (int i = 0; i < nodes; i++) order[i] = i;
|
||||||
|
java.util.Random rnd = new java.util.Random(20260730L);
|
||||||
|
for (int i = nodes - 1; i > 0; i--) {
|
||||||
|
int j = rnd.nextInt(i + 1);
|
||||||
|
int t = order[i]; order[i] = order[j]; order[j] = t;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < nodes - 1; i++) {
|
||||||
|
all[order[i]].next = all[order[i + 1]];
|
||||||
|
}
|
||||||
|
head = all[order[0]];
|
||||||
|
|
||||||
|
primitives = new int[nodes];
|
||||||
|
for (int i = 0; i < nodes; i++) primitives[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reference-load dominated: one load barrier per step under ZGC, none under G1. */
|
||||||
|
@Benchmark
|
||||||
|
public int chaseReferences() {
|
||||||
|
int sum = 0;
|
||||||
|
Node n = head;
|
||||||
|
while (n != null) {
|
||||||
|
sum += n.value;
|
||||||
|
n = n.next; // <-- ZGC load barrier
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Primitive-load control: same element count, no reference loads, no barriers on any GC. */
|
||||||
|
@Benchmark
|
||||||
|
public int sumPrimitives() {
|
||||||
|
int sum = 0;
|
||||||
|
for (int v : primitives) sum += v;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "hot field" read of the same reference over and over. ZGC's barrier has a fast path that is
|
||||||
|
* just a test-and-branch when the pointer is already good, so this variant shows the *floor*
|
||||||
|
* of the barrier cost -- what you pay when nothing needs fixing up.
|
||||||
|
*/
|
||||||
|
@Benchmark
|
||||||
|
public void readSameReferenceRepeatedly(Blackhole bh) {
|
||||||
|
Node n = head;
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
bh.consume(n.next); // <-- barrier, but always on an already-good pointer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
jmh/src/main/java/com/ankurm/zgc/jmh/PromotionBench.java
Normal file
75
jmh/src/main/java/com/ankurm/zgc/jmh/PromotionBench.java
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ankurm.zgc.jmh;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.*;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Medium-lived objects: the workload that decides whether a generational collector actually helps.
|
||||||
|
*
|
||||||
|
* <h2>Why this exists</h2>
|
||||||
|
* The weak generational hypothesis says most objects die young. When that holds, a young collection
|
||||||
|
* is nearly free: the collector copies out the few survivors and declares the whole young region
|
||||||
|
* empty. When it does <em>not</em> hold -- when a large fraction of objects survive long enough to
|
||||||
|
* be promoted -- a generational collector does its work twice: once tracing them in the young
|
||||||
|
* generation, once again after promotion.
|
||||||
|
*
|
||||||
|
* <p>Real services sit in the middle. A request-scoped object graph that lives for the duration of
|
||||||
|
* a slow downstream call, a batch accumulating results, a connection buffer pool refilling itself:
|
||||||
|
* all of these produce objects that are neither immediately dead nor permanently live. This
|
||||||
|
* benchmark parameterises exactly that.
|
||||||
|
*
|
||||||
|
* <p>{@code survivorDepth} is the number of objects held alive at any moment. Depth 0 is pure
|
||||||
|
* young garbage (the friendly case). Large depths force promotion, and on generational ZGC that
|
||||||
|
* means the promoted objects must also be handled by the old-generation cycle.
|
||||||
|
*
|
||||||
|
* <p>This is also the benchmark where the JDK 25 G1 remembered-set improvements show up: with a
|
||||||
|
* high promotion rate, G1's mixed collections have more cross-region references to track.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* java -jar target/benchmarks.jar PromotionBench -prof gc -jvmArgs "-XX:+UseZGC -Xms2g -Xmx2g"
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||||
|
@State(Scope.Thread)
|
||||||
|
@Warmup(iterations = 3, time = 3)
|
||||||
|
@Measurement(iterations = 5, time = 3)
|
||||||
|
@Fork(1)
|
||||||
|
public class PromotionBench {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many previously allocated payloads stay reachable.
|
||||||
|
* 0 -> every object dies immediately (best case for any generational GC)
|
||||||
|
* 4096 -> a modest working set that mostly dies in the young generation
|
||||||
|
* 262144 -> a large working set that forces sustained promotion into the old generation
|
||||||
|
*/
|
||||||
|
@Param({"0", "4096", "262144"})
|
||||||
|
public int survivorDepth;
|
||||||
|
|
||||||
|
@Param({"512"})
|
||||||
|
public int payloadBytes;
|
||||||
|
|
||||||
|
private Deque<byte[]> survivors;
|
||||||
|
|
||||||
|
@Setup(Level.Iteration)
|
||||||
|
public void setup() {
|
||||||
|
survivors = new ArrayDeque<>(Math.max(16, survivorDepth));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public byte[] allocateWithSurvival() {
|
||||||
|
byte[] payload = new byte[payloadBytes];
|
||||||
|
payload[0] = 1;
|
||||||
|
if (survivorDepth == 0) {
|
||||||
|
return payload; // dies at the end of this method
|
||||||
|
}
|
||||||
|
survivors.addLast(payload);
|
||||||
|
if (survivors.size() > survivorDepth) {
|
||||||
|
return survivors.pollFirst(); // evicted after surviving `survivorDepth` allocations
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
104
jmh/src/main/java/com/ankurm/zgc/jmh/StoreBarrierBench.java
Normal file
104
jmh/src/main/java/com/ankurm/zgc/jmh/StoreBarrierBench.java
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package com.ankurm.zgc.jmh;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.*;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Isolates the cost of the <b>write/store barrier</b> -- the mechanism every generational collector
|
||||||
|
* needs in order to collect the young generation without scanning the old one.
|
||||||
|
*
|
||||||
|
* <h2>The problem being solved</h2>
|
||||||
|
* A young collection wants to mark only young objects. But an old object may hold the only
|
||||||
|
* reference to a young object. If the collector ignored the old generation entirely it would free a
|
||||||
|
* live object. Scanning the whole old generation would defeat the point of a young collection. So
|
||||||
|
* every generational collector intercepts reference stores and records the ones that create an
|
||||||
|
* old-to-young edge.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>G1</b> dirties a <em>card</em> (a 512-byte granule of the heap) and hands it to
|
||||||
|
* concurrent refinement threads, which update per-region remembered sets. G1's barrier is
|
||||||
|
* famously the more expensive of the two: it has a pre-write component for SATB marking and
|
||||||
|
* a post-write component for card marking. JDK 24 moved G1's barrier expansion late into C2
|
||||||
|
* (JDK-8342382, "late barrier expansion"), which lets the optimiser see through more of it.
|
||||||
|
* <li><b>ZGC</b> before JEP 439 had <em>no</em> store barrier at all -- which is exactly why
|
||||||
|
* non-generational ZGC had to mark the entire heap every cycle. Generational ZGC added one,
|
||||||
|
* feeding per-page remembered sets stored as double-buffered bitmaps.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>What this benchmark does</h2>
|
||||||
|
* {@link #storeOldToYoung()} writes a freshly allocated (young) object into an array that has been
|
||||||
|
* alive since {@code @Setup} and has therefore been promoted -- the barrier fires and records a
|
||||||
|
* cross-generational edge. {@link #storeNullIntoOld()} performs an identically shaped reference
|
||||||
|
* store whose value is {@code null}, which cannot create a cross-generational edge; comparing them
|
||||||
|
* separates "the barrier ran" from "the barrier had work to do". {@link #storePrimitiveIntoOld()}
|
||||||
|
* is the control: an {@code int} store into a long-lived array, which no collector needs to track.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* java -jar target/benchmarks.jar StoreBarrierBench -jvmArgs "-XX:+UseZGC -Xms2g -Xmx2g"
|
||||||
|
* java -jar target/benchmarks.jar StoreBarrierBench -jvmArgs "-XX:+UseG1GC -Xms2g -Xmx2g"
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@BenchmarkMode(Mode.AverageTime)
|
||||||
|
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
@Warmup(iterations = 3, time = 3)
|
||||||
|
@Measurement(iterations = 5, time = 3)
|
||||||
|
@Fork(1)
|
||||||
|
public class StoreBarrierBench {
|
||||||
|
|
||||||
|
/** Size of the long-lived array being written into. Larger = more cards / more pages touched. */
|
||||||
|
@Param({"1048576"})
|
||||||
|
public int slots;
|
||||||
|
|
||||||
|
private Object[] oldArray;
|
||||||
|
private int[] oldPrimitiveArray;
|
||||||
|
private int cursor;
|
||||||
|
|
||||||
|
@Setup(Level.Trial)
|
||||||
|
public void setup() {
|
||||||
|
oldArray = new Object[slots];
|
||||||
|
oldPrimitiveArray = new int[slots];
|
||||||
|
for (int i = 0; i < slots; i++) {
|
||||||
|
oldArray[i] = new Object();
|
||||||
|
}
|
||||||
|
// Force the array (and its contents) into the old generation before measuring, otherwise
|
||||||
|
// the first iterations measure young-to-young stores, which no barrier needs to record.
|
||||||
|
System.gc();
|
||||||
|
System.gc();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nextSlot() {
|
||||||
|
// A strided walk rather than a sequential one, so consecutive stores land on different
|
||||||
|
// cards / pages. A sequential walk would dirty the same card repeatedly and the barrier's
|
||||||
|
// "already dirty" fast path would hide most of the cost.
|
||||||
|
cursor = (cursor + 4099) & (slots - 1);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Old-to-young reference store: the barrier fires AND has real work to record. */
|
||||||
|
@Benchmark
|
||||||
|
public void storeOldToYoung() {
|
||||||
|
oldArray[nextSlot()] = new Object();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reference store of null: barrier code still runs, but records no cross-generational edge. */
|
||||||
|
@Benchmark
|
||||||
|
public void storeNullIntoOld() {
|
||||||
|
oldArray[nextSlot()] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Old-to-old reference store: no new cross-generational edge is created. */
|
||||||
|
@Benchmark
|
||||||
|
public void storeOldToOld() {
|
||||||
|
oldArray[nextSlot()] = oldArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Control: a primitive store into an equally long-lived array. No barrier on any collector. */
|
||||||
|
@Benchmark
|
||||||
|
public int storePrimitiveIntoOld() {
|
||||||
|
int slot = nextSlot();
|
||||||
|
oldPrimitiveArray[slot] = slot;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
latency/src/com/ankurm/zgc/latency/Config.java
Normal file
87
latency/src/com/ankurm/zgc/latency/Config.java
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package com.ankurm.zgc.latency;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All knobs of the latency harness in one place, so a run is fully described by one line of output.
|
||||||
|
*
|
||||||
|
* <p>Every value is a system property so that the JVM command line -- which is where the GC is
|
||||||
|
* selected anyway -- is the single source of truth for a run:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* java -XX:+UseZGC -Xms2g -Xmx2g -Drate=40000 -Dseconds=60 ...
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public record Config(
|
||||||
|
int threads,
|
||||||
|
int ratePerSecond,
|
||||||
|
int warmupSeconds,
|
||||||
|
int measureSeconds,
|
||||||
|
int liveSetEntries,
|
||||||
|
int liveSetPayloadBytes,
|
||||||
|
int perRequestGarbageBytes,
|
||||||
|
int readsPerRequest,
|
||||||
|
double mutationProbability,
|
||||||
|
long seed) {
|
||||||
|
|
||||||
|
public static Config fromSystemProperties() {
|
||||||
|
return new Config(
|
||||||
|
// Each worker busy-spins between arrivals (see LatencyHarness#parkUntil), so it
|
||||||
|
// occupies a core. A third of the logical CPUs leaves room for the collector's own
|
||||||
|
// concurrent threads -- starving those would make ZGC look bad for the wrong reason.
|
||||||
|
intProp("threads", Math.max(2, Runtime.getRuntime().availableProcessors() / 3)),
|
||||||
|
intProp("rate", 20_000),
|
||||||
|
intProp("warmupSeconds", 15),
|
||||||
|
intProp("seconds", 30),
|
||||||
|
// 150k entries x ~4 KB ~= 600 MB of genuinely live data in a 2 GB heap. The point is
|
||||||
|
// to make the OLD generation expensive to mark, which is where G1 and ZGC diverge.
|
||||||
|
intProp("liveSetEntries", 150_000),
|
||||||
|
intProp("liveSetPayloadBytes", 4_096),
|
||||||
|
// 20k req/s x 16 KB ~= 320 MB/s of young garbage: a realistic allocation rate for a
|
||||||
|
// busy JVM service, and high enough that young collections actually happen.
|
||||||
|
intProp("garbageBytes", 16_384),
|
||||||
|
intProp("readsPerRequest", 64),
|
||||||
|
doubleProp("mutationProbability", 0.20),
|
||||||
|
longProp("seed", 20260730L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bytes of young garbage this configuration creates per second, ignoring promotion. */
|
||||||
|
public long allocationRateBytesPerSecond() {
|
||||||
|
return (long) ratePerSecond * perRequestGarbageBytes
|
||||||
|
+ (long) (ratePerSecond * mutationProbability * liveSetPayloadBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requests each worker thread must issue during the measured window. */
|
||||||
|
public long requestsPerThread() {
|
||||||
|
return (long) measureSeconds * ratePerSecond / threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nanoseconds between two request arrivals *for one worker thread*. */
|
||||||
|
public long interArrivalNanos() {
|
||||||
|
return 1_000_000_000L * threads / ratePerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String describe() {
|
||||||
|
return """
|
||||||
|
threads=%d rate=%,d/s warmup=%ds measure=%ds
|
||||||
|
liveSet=%,d entries x %,d B (~%,d MB live)
|
||||||
|
garbage=%,d B/request reads=%d/request mutationProbability=%.2f
|
||||||
|
nominal allocation rate ~%,d MB/s seed=%d"""
|
||||||
|
.formatted(threads, ratePerSecond, warmupSeconds, measureSeconds,
|
||||||
|
liveSetEntries, liveSetPayloadBytes,
|
||||||
|
(long) liveSetEntries * liveSetPayloadBytes / 1024 / 1024,
|
||||||
|
perRequestGarbageBytes, readsPerRequest, mutationProbability,
|
||||||
|
allocationRateBytesPerSecond() / 1024 / 1024, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intProp(String k, int def) {
|
||||||
|
return Integer.getInteger(k, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long longProp(String k, long def) {
|
||||||
|
return Long.getLong(k, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double doubleProp(String k, double def) {
|
||||||
|
String v = System.getProperty(k);
|
||||||
|
return v == null ? def : Double.parseDouble(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
latency/src/com/ankurm/zgc/latency/GcObserver.java
Normal file
68
latency/src/com/ankurm/zgc/latency/GcObserver.java
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ankurm.zgc.latency;
|
||||||
|
|
||||||
|
import java.lang.management.GarbageCollectorMXBean;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshots {@link GarbageCollectorMXBean} counters so a run can report GC activity in the same
|
||||||
|
* output as the latency numbers.
|
||||||
|
*
|
||||||
|
* <p><b>The trap this class exists to expose.</b> On ZGC the {@code getCollectionTime()} value is
|
||||||
|
* <em>not</em> stop-the-world time. The "ZGC Major Cycles" / "ZGC Minor Cycles" beans report the
|
||||||
|
* duration of the whole concurrent <em>cycle</em>, most of which runs alongside your application.
|
||||||
|
* The separate "ZGC Major Pauses" / "ZGC Minor Pauses" beans report the actual stop-the-world time.
|
||||||
|
* On G1 there is only one bean per generation and it <em>is</em> pause time.
|
||||||
|
*
|
||||||
|
* <p>Comparing G1's pause bean against ZGC's cycle bean is the single most common way people
|
||||||
|
* accidentally "prove" that ZGC is slower. This class keeps them apart on purpose.
|
||||||
|
*/
|
||||||
|
public final class GcObserver {
|
||||||
|
|
||||||
|
private final Map<String, long[]> baseline = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public GcObserver() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-baselines all counters. Call after warm-up so warm-up GCs are not counted. */
|
||||||
|
public void reset() {
|
||||||
|
baseline.clear();
|
||||||
|
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||||
|
baseline.put(gc.getName(), new long[]{gc.getCollectionCount(), gc.getCollectionTime()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String report() {
|
||||||
|
StringBuilder sb = new StringBuilder("GC activity during the measured window\n");
|
||||||
|
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||||
|
long[] base = baseline.getOrDefault(gc.getName(), new long[]{0, 0});
|
||||||
|
long collections = gc.getCollectionCount() - base[0];
|
||||||
|
long millis = gc.getCollectionTime() - base[1];
|
||||||
|
String kind = isPauseBean(gc.getName()) ? "stop-the-world" : "cycle (mostly concurrent)";
|
||||||
|
sb.append(" %-22s collections=%-6d time=%,7d ms [%s]%n"
|
||||||
|
.formatted(gc.getName(), collections, millis, kind));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classifies a bean as reporting stop-the-world time or concurrent time.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>ZGC</b> exposes four beans: "ZGC Minor/Major Cycles" (concurrent cycle duration) and
|
||||||
|
* "ZGC Minor/Major Pauses" (real stop-the-world time). Only the latter are pauses.</li>
|
||||||
|
* <li><b>G1</b> exposes "G1 Young Generation" and "G1 Old Generation", which are pauses, plus
|
||||||
|
* "G1 Concurrent GC", which -- despite sitting in the same bean list -- reports the
|
||||||
|
* concurrent marking cycle and is <em>not</em> stop-the-world.</li>
|
||||||
|
* <li><b>Parallel and Serial</b> beans are all pauses.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
private static boolean isPauseBean(String name) {
|
||||||
|
String n = name.toLowerCase();
|
||||||
|
if (n.contains("concurrent")) return false;
|
||||||
|
if (n.startsWith("zgc")) return n.contains("pause");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
latency/src/com/ankurm/zgc/latency/Histogram.java
Normal file
102
latency/src/com/ankurm/zgc/latency/Histogram.java
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package com.ankurm.zgc.latency;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deliberately dumb latency recorder: it keeps every raw sample in a pre-allocated
|
||||||
|
* {@code long[]} and sorts once at the end.
|
||||||
|
*
|
||||||
|
* <p>Why not HdrHistogram? Two reasons, both about honesty:
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li><b>No dependency.</b> This repository must run with nothing but a JDK.</li>
|
||||||
|
* <li><b>No bucketing error.</b> HdrHistogram is excellent, but it quantises values. When you are
|
||||||
|
* arguing about whether p99.99 is 3 ms or 11 ms, exact samples remove one thing to argue
|
||||||
|
* about.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>The array is allocated <em>before</em> the measured window and never grows, so the recorder
|
||||||
|
* itself contributes zero allocation to the workload it is measuring. That matters here: a
|
||||||
|
* histogram that allocates during a GC benchmark is measuring itself.
|
||||||
|
*/
|
||||||
|
public final class Histogram {
|
||||||
|
|
||||||
|
private final long[] samples;
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
public Histogram(int capacity) {
|
||||||
|
this.samples = new long[capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records one sample. Silently drops overflow rather than resizing mid-measurement. */
|
||||||
|
public void record(long nanos) {
|
||||||
|
if (count < samples.length) {
|
||||||
|
samples[count++] = nanos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int count() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sorts in place. Call exactly once, after the measured window. */
|
||||||
|
public Histogram freeze() {
|
||||||
|
Arrays.sort(samples, 0, count);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param p percentile in [0, 100], e.g. 99.99 */
|
||||||
|
public long percentileNanos(double p) {
|
||||||
|
if (count == 0) return 0;
|
||||||
|
int idx = (int) Math.ceil(p / 100.0 * count) - 1;
|
||||||
|
return samples[Math.clamp(idx, 0, count - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public long maxNanos() {
|
||||||
|
return count == 0 ? 0 : samples[count - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public double meanMillis() {
|
||||||
|
long sum = 0;
|
||||||
|
for (int i = 0; i < count; i++) sum += samples[i];
|
||||||
|
return count == 0 ? 0 : sum / (double) count / 1_000_000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merges another histogram's samples into this one (used to combine per-thread recorders). */
|
||||||
|
public void mergeFrom(Histogram other) {
|
||||||
|
int n = Math.min(other.count, samples.length - count);
|
||||||
|
System.arraycopy(other.samples, 0, samples, count, n);
|
||||||
|
count += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String report(String label) {
|
||||||
|
return """
|
||||||
|
%s (n=%,d)
|
||||||
|
mean %8.3f ms
|
||||||
|
p50 %8.3f ms
|
||||||
|
p90 %8.3f ms
|
||||||
|
p99 %8.3f ms
|
||||||
|
p99.9 %8.3f ms
|
||||||
|
p99.99 %8.3f ms
|
||||||
|
max %8.3f ms"""
|
||||||
|
.formatted(label, count, meanMillis(),
|
||||||
|
ms(percentileNanos(50)), ms(percentileNanos(90)), ms(percentileNanos(99)),
|
||||||
|
ms(percentileNanos(99.9)), ms(percentileNanos(99.99)), ms(maxNanos()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Machine-readable one-liner, so runs can be diffed and pasted into a table. */
|
||||||
|
public String csvRow(String collector) {
|
||||||
|
return "%s,%d,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f".formatted(
|
||||||
|
collector, count, meanMillis(),
|
||||||
|
ms(percentileNanos(50)), ms(percentileNanos(90)), ms(percentileNanos(99)),
|
||||||
|
ms(percentileNanos(99.9)), ms(percentileNanos(99.99)), ms(maxNanos()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String csvHeader() {
|
||||||
|
return "collector,n,mean_ms,p50_ms,p90_ms,p99_ms,p999_ms,p9999_ms,max_ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ms(long nanos) {
|
||||||
|
return nanos / 1_000_000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
258
latency/src/com/ankurm/zgc/latency/LatencyHarness.java
Normal file
258
latency/src/com/ankurm/zgc/latency/LatencyHarness.java
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package com.ankurm.zgc.latency;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An <b>open-loop</b> latency harness for comparing collectors.
|
||||||
|
*
|
||||||
|
* <p>This is the part of a GC comparison that most benchmarks get wrong, so it is worth being
|
||||||
|
* explicit about what "open loop" means and why it changes the answer.
|
||||||
|
*
|
||||||
|
* <h2>Closed loop (wrong for this question)</h2>
|
||||||
|
* A closed-loop benchmark has N threads in a {@code while(true) { doRequest(); }} loop. If a GC
|
||||||
|
* pause freezes the application for 50 ms, those threads simply do not issue requests during the
|
||||||
|
* pause. When they resume, they measure one slow request each -- and then the throughput number
|
||||||
|
* quietly absorbs the rest. The pause is real, your users felt it as 50 ms of queueing, and the
|
||||||
|
* benchmark reports it as a handful of samples. Gil Tene named this <b>coordinated omission</b>:
|
||||||
|
* the load generator has "coordinated" with the system under test to stop sending load exactly when
|
||||||
|
* the system was least able to serve it.
|
||||||
|
*
|
||||||
|
* <h2>Open loop (what this harness does)</h2>
|
||||||
|
* Each worker thread has a fixed arrival <em>schedule</em> computed up front:
|
||||||
|
* {@code intended[i] = start + i * interArrivalNanos}. Latency for request {@code i} is measured
|
||||||
|
* from {@code intended[i]}, not from when the thread actually got around to it. A 50 ms pause
|
||||||
|
* therefore shows up as ~2000 requests at 40k/s whose latency includes the queueing delay they
|
||||||
|
* really suffered -- which is what a load balancer, and a user, would have seen.
|
||||||
|
*
|
||||||
|
* <p>The harness reports both numbers side by side so the difference is visible rather than
|
||||||
|
* asserted:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>response time</b> = completion − intended arrival (the honest number)</li>
|
||||||
|
* <li><b>service time</b> = completion − actual start (the flattering number)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Running it</h2>
|
||||||
|
* <pre>
|
||||||
|
* javac -d out $(find latency/src -name '*.java')
|
||||||
|
* java -XX:+UseZGC -Xms2g -Xmx2g -cp out com.ankurm.zgc.latency.LatencyHarness
|
||||||
|
* java -XX:+UseG1GC -Xms2g -Xmx2g -cp out com.ankurm.zgc.latency.LatencyHarness
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public final class LatencyHarness {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Config cfg = Config.fromSystemProperties();
|
||||||
|
String collector = detectCollector();
|
||||||
|
|
||||||
|
System.out.println("=".repeat(78));
|
||||||
|
System.out.println("ZGC / G1 open-loop latency harness");
|
||||||
|
System.out.println("=".repeat(78));
|
||||||
|
System.out.println("collector : " + collector);
|
||||||
|
System.out.println("java : " + System.getProperty("java.vm.version"));
|
||||||
|
System.out.println(cfg.describe());
|
||||||
|
System.out.println("-".repeat(78));
|
||||||
|
|
||||||
|
System.out.print("building live set ... ");
|
||||||
|
long t0 = System.nanoTime();
|
||||||
|
LiveSet liveSet = new LiveSet(cfg.liveSetEntries(), cfg.liveSetPayloadBytes());
|
||||||
|
System.out.printf("%,d entries, ~%,d MB live, took %,d ms%n",
|
||||||
|
liveSet.entries(),
|
||||||
|
liveSet.approximateLiveBytes() / 1024 / 1024,
|
||||||
|
(System.nanoTime() - t0) / 1_000_000);
|
||||||
|
|
||||||
|
// ---- Warm-up -------------------------------------------------------------------
|
||||||
|
// Two things warm up here: the JIT (C2 needs to compile the request path) and the heap
|
||||||
|
// (ZGC and G1 both need a few cycles before their heuristics settle). Measuring before
|
||||||
|
// both have happened produces a benchmark of the interpreter, not of the collector.
|
||||||
|
System.out.printf("warming up for %d s at %,d req/s ...%n", cfg.warmupSeconds(), cfg.ratePerSecond());
|
||||||
|
runPhase(cfg, liveSet, cfg.warmupSeconds(), null, null);
|
||||||
|
|
||||||
|
// ---- Measure -------------------------------------------------------------------
|
||||||
|
GcObserver gcObserver = new GcObserver(); // baseline AFTER warm-up
|
||||||
|
int capacity = (int) Math.min(Integer.MAX_VALUE - 8L,
|
||||||
|
(long) cfg.measureSeconds() * cfg.ratePerSecond() + 1024);
|
||||||
|
Histogram responseTime = new Histogram(capacity);
|
||||||
|
Histogram serviceTime = new Histogram(capacity);
|
||||||
|
|
||||||
|
System.out.printf("measuring for %d s at %,d req/s ...%n", cfg.measureSeconds(), cfg.ratePerSecond());
|
||||||
|
long wallStart = System.nanoTime();
|
||||||
|
runPhase(cfg, liveSet, cfg.measureSeconds(), responseTime, serviceTime);
|
||||||
|
long wallNanos = System.nanoTime() - wallStart;
|
||||||
|
|
||||||
|
responseTime.freeze();
|
||||||
|
serviceTime.freeze();
|
||||||
|
|
||||||
|
// ---- Report --------------------------------------------------------------------
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("-".repeat(78));
|
||||||
|
System.out.printf("achieved throughput : %,.0f req/s over %.1f s%n",
|
||||||
|
responseTime.count() / (wallNanos / 1e9), wallNanos / 1e9);
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(responseTime.report("RESPONSE TIME (from intended arrival -- the honest number)"));
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(serviceTime.report("SERVICE TIME (from actual start -- hides queueing)"));
|
||||||
|
System.out.println();
|
||||||
|
System.out.printf("coordinated-omission gap at p99.9 : %.3f ms response vs %.3f ms service (%.1fx)%n",
|
||||||
|
responseTime.percentileNanos(99.9) / 1e6,
|
||||||
|
serviceTime.percentileNanos(99.9) / 1e6,
|
||||||
|
safeRatio(responseTime.percentileNanos(99.9), serviceTime.percentileNanos(99.9)));
|
||||||
|
System.out.println();
|
||||||
|
System.out.print(gcObserver.report());
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("CSV (response time)");
|
||||||
|
System.out.println(Histogram.csvHeader());
|
||||||
|
System.out.println(responseTime.csvRow(collector));
|
||||||
|
|
||||||
|
// Keep the live set reachable to the very end, otherwise a clever JIT could shorten its
|
||||||
|
// lifetime and quietly delete the old generation we spent the whole run building.
|
||||||
|
System.out.println("\nliveSet checksum: " + liveSet.touch());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs one phase. Passing {@code null} histograms makes it a warm-up phase (no recording,
|
||||||
|
* and therefore no measurement bias from recording code that is not yet JIT-compiled).
|
||||||
|
*/
|
||||||
|
private static void runPhase(Config cfg, LiveSet liveSet, int seconds,
|
||||||
|
Histogram responseOut, Histogram serviceOut) throws Exception {
|
||||||
|
|
||||||
|
int threads = cfg.threads();
|
||||||
|
long interArrival = cfg.interArrivalNanos();
|
||||||
|
long perThread = (long) seconds * cfg.ratePerSecond() / threads;
|
||||||
|
|
||||||
|
CountDownLatch ready = new CountDownLatch(threads);
|
||||||
|
CountDownLatch go = new CountDownLatch(1);
|
||||||
|
List<Thread> workers = new ArrayList<>(threads);
|
||||||
|
List<Histogram> responseParts = new ArrayList<>(threads);
|
||||||
|
List<Histogram> serviceParts = new ArrayList<>(threads);
|
||||||
|
|
||||||
|
for (int t = 0; t < threads; t++) {
|
||||||
|
Histogram r = responseOut == null ? null : new Histogram((int) perThread + 16);
|
||||||
|
Histogram s = serviceOut == null ? null : new Histogram((int) perThread + 16);
|
||||||
|
if (r != null) { responseParts.add(r); serviceParts.add(s); }
|
||||||
|
|
||||||
|
Thread worker = new Thread(() -> {
|
||||||
|
ready.countDown();
|
||||||
|
try {
|
||||||
|
go.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long start = System.nanoTime();
|
||||||
|
for (long i = 0; i < perThread; i++) {
|
||||||
|
long intended = start + i * interArrival;
|
||||||
|
parkUntil(intended);
|
||||||
|
long actualStart = System.nanoTime();
|
||||||
|
handleRequest(cfg, liveSet);
|
||||||
|
long done = System.nanoTime();
|
||||||
|
if (r != null) {
|
||||||
|
r.record(done - intended); // includes queueing caused by GC pauses
|
||||||
|
s.record(done - actualStart); // the number a closed-loop harness reports
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "worker-" + t);
|
||||||
|
worker.setDaemon(true);
|
||||||
|
workers.add(worker);
|
||||||
|
worker.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
ready.await();
|
||||||
|
go.countDown();
|
||||||
|
for (Thread w : workers) w.join();
|
||||||
|
|
||||||
|
if (responseOut != null) {
|
||||||
|
for (int i = 0; i < responseParts.size(); i++) {
|
||||||
|
responseOut.mergeFrom(responseParts.get(i).freeze());
|
||||||
|
serviceOut.mergeFrom(serviceParts.get(i).freeze());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One "request": allocate short-lived garbage, read from the live set, and occasionally write
|
||||||
|
* a new object into it. This is a deliberately ordinary shape -- a request handler that builds
|
||||||
|
* a response buffer, reads a cache, and sometimes refreshes a cache entry.
|
||||||
|
*/
|
||||||
|
private static void handleRequest(Config cfg, LiveSet liveSet) {
|
||||||
|
ThreadLocalRandom rnd = ThreadLocalRandom.current();
|
||||||
|
|
||||||
|
// Young garbage: dies before the next young collection. This is the cheap part for every
|
||||||
|
// generational collector, and the whole reason generational ZGC exists.
|
||||||
|
byte[] scratch = new byte[cfg.perRequestGarbageBytes()];
|
||||||
|
scratch[0] = (byte) rnd.nextInt();
|
||||||
|
int sum = scratch[0];
|
||||||
|
|
||||||
|
// Reads from the old generation: on ZGC every one of these goes through a load barrier.
|
||||||
|
// The count is deliberately high enough that a request does measurable work (tens of
|
||||||
|
// microseconds) rather than a few nanoseconds -- a harness whose requests are shorter than
|
||||||
|
// its own timer resolution measures the timer.
|
||||||
|
int reads = cfg.readsPerRequest();
|
||||||
|
for (int i = 0; i < reads; i++) {
|
||||||
|
sum += liveSet.touch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old-to-young store: on ZGC this goes through the store barrier added by JEP 439 and
|
||||||
|
// updates a remembered set. On G1 it dirties a card.
|
||||||
|
if (rnd.nextDouble() < cfg.mutationProbability()) {
|
||||||
|
byte[] evicted = liveSet.mutate();
|
||||||
|
sum += evicted.length & 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Blackhole.consume(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleeps until an absolute {@link System#nanoTime()} deadline.
|
||||||
|
*
|
||||||
|
* <h3>Why this is not simply {@code LockSupport.parkNanos(remaining)}</h3>
|
||||||
|
* The default Windows timer tick is ~15.6 ms and, even with the multimedia timer raised, a
|
||||||
|
* park rounds up to roughly 1 ms. At a 200 microsecond inter-arrival interval that means every
|
||||||
|
* park overshoots by several intervals. The thread then blasts through the backlog, parks,
|
||||||
|
* overshoots again -- and the harness reports a p50 of half a millisecond on a request that
|
||||||
|
* takes three microseconds. The first version of this file did exactly that and it looked like
|
||||||
|
* a real GC effect. It was the sleep.
|
||||||
|
*
|
||||||
|
* <p>So: only park when the deadline is far enough away that a 1 ms overshoot does not matter
|
||||||
|
* ({@link #PARK_THRESHOLD_NANOS}), and busy-spin otherwise. Spinning costs a core per worker
|
||||||
|
* thread, which is why {@code threads} defaults to a fraction of the CPU count -- the GC needs
|
||||||
|
* cores too, and starving its concurrent threads would be a different kind of measurement lie.
|
||||||
|
*
|
||||||
|
* <p>If the deadline has already passed the method returns immediately: the request is
|
||||||
|
* <em>late</em>, and that lateness is precisely what we want in the response-time histogram.
|
||||||
|
*/
|
||||||
|
private static final long PARK_THRESHOLD_NANOS = 3_000_000L; // 3 ms >> Windows timer slack
|
||||||
|
|
||||||
|
private static void parkUntil(long deadlineNanos) {
|
||||||
|
long remaining = deadlineNanos - System.nanoTime();
|
||||||
|
if (remaining <= 0) return; // already behind schedule -- do not sleep
|
||||||
|
if (remaining > PARK_THRESHOLD_NANOS) {
|
||||||
|
LockSupport.parkNanos(remaining - PARK_THRESHOLD_NANOS);
|
||||||
|
}
|
||||||
|
while (System.nanoTime() < deadlineNanos) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double safeRatio(long a, long b) {
|
||||||
|
return b == 0 ? 0 : (double) a / b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String detectCollector() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (var gc : java.lang.management.ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||||
|
if (!sb.isEmpty()) sb.append(" + ");
|
||||||
|
sb.append(gc.getName());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal dead-code sink; JMH's Blackhole is not available in a dependency-free module. */
|
||||||
|
static final class Blackhole {
|
||||||
|
static volatile int sink;
|
||||||
|
static void consume(int v) { sink = v; }
|
||||||
|
}
|
||||||
|
}
|
||||||
77
latency/src/com/ankurm/zgc/latency/LiveSet.java
Normal file
77
latency/src/com/ankurm/zgc/latency/LiveSet.java
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package com.ankurm.zgc.latency;
|
||||||
|
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The old generation, made explicit.
|
||||||
|
*
|
||||||
|
* <p>A GC benchmark that only allocates short-lived garbage measures almost nothing interesting:
|
||||||
|
* every collector on earth is good at throwing away objects that die immediately (that is the weak
|
||||||
|
* generational hypothesis doing the work, not the collector). The interesting question is what
|
||||||
|
* happens when there is a large, long-lived, *slowly mutating* object graph -- a cache, a session
|
||||||
|
* store, an in-memory index -- because that is what has to be marked, and marking cost is what
|
||||||
|
* separates collectors.
|
||||||
|
*
|
||||||
|
* <p>This class holds {@code entries} byte arrays alive for the whole run. Periodically a slot is
|
||||||
|
* overwritten with a freshly allocated array. That single store is the most important line in this
|
||||||
|
* repository:
|
||||||
|
*
|
||||||
|
* <pre>{@code slots[i] = newPayload;}</pre>
|
||||||
|
*
|
||||||
|
* <p>It writes a reference to a <b>young</b> object into a field of an <b>old</b> object. Every
|
||||||
|
* generational collector needs to know that this happened, or a young collection would wrongly
|
||||||
|
* conclude the new object is unreachable. G1 records it in a card table + remembered sets; ZGC --
|
||||||
|
* since JEP 439 -- records it with a <b>store barrier</b> feeding per-page remembered sets. Before
|
||||||
|
* generational ZGC there was no store barrier at all, which is exactly why old ZGC had to mark the
|
||||||
|
* entire heap on every cycle.
|
||||||
|
*
|
||||||
|
* <p>Turning {@code mutationProbability} up and down changes how much cross-generational traffic
|
||||||
|
* the collector sees, and is the knob that makes the ZGC-vs-G1 gap open and close.
|
||||||
|
*/
|
||||||
|
public final class LiveSet {
|
||||||
|
|
||||||
|
private final byte[][] slots;
|
||||||
|
private final int payloadBytes;
|
||||||
|
|
||||||
|
public LiveSet(int entries, int payloadBytes) {
|
||||||
|
this.slots = new byte[entries][];
|
||||||
|
this.payloadBytes = payloadBytes;
|
||||||
|
// Fill up front so the measured window starts with a fully populated old generation
|
||||||
|
// rather than measuring the ramp-up.
|
||||||
|
for (int i = 0; i < entries; i++) {
|
||||||
|
slots[i] = new byte[payloadBytes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites one random slot -- an old-to-young reference store.
|
||||||
|
*
|
||||||
|
* @return the previous occupant, returned only so the JIT cannot delete the store as dead code
|
||||||
|
*/
|
||||||
|
public byte[] mutate() {
|
||||||
|
int i = ThreadLocalRandom.current().nextInt(slots.length);
|
||||||
|
byte[] previous = slots[i];
|
||||||
|
byte[] payload = new byte[payloadBytes];
|
||||||
|
// Touch a few bytes so the array cannot be optimised into a zero-fill the JIT elides.
|
||||||
|
payload[0] = 1;
|
||||||
|
payload[payload.length - 1] = 2;
|
||||||
|
slots[i] = payload; // <-- the store barrier fires here
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads one random slot -- a load barrier fires here on every ZGC read. */
|
||||||
|
public int touch() {
|
||||||
|
int i = ThreadLocalRandom.current().nextInt(slots.length);
|
||||||
|
byte[] payload = slots[i]; // <-- ZGC load barrier fires here
|
||||||
|
return payload.length == 0 ? 0 : payload[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int entries() {
|
||||||
|
return slots.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long approximateLiveBytes() {
|
||||||
|
// 16 bytes of object header + length for each array, rounded, plus the slots array itself.
|
||||||
|
return (long) slots.length * (payloadBytes + 16L) + slots.length * 8L;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
results/01-env-g1.txt
Normal file
34
results/01-env-g1.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
=== Runtime ===
|
||||||
|
java.version : 25.0.3
|
||||||
|
java.vm.name : Java HotSpot(TM) 64-Bit Server VM
|
||||||
|
java.vm.version : 25.0.3+9-LTS-195
|
||||||
|
java.vendor : Oracle Corporation
|
||||||
|
os.name / os.arch : Windows 11 / amd64
|
||||||
|
availableProcessors : 12
|
||||||
|
maxMemory (-Xmx) : 2,147,483,648 bytes (2048 MB)
|
||||||
|
totalMemory (now) : 2,147,483,648 bytes (2048 MB)
|
||||||
|
|
||||||
|
=== Collector in use ===
|
||||||
|
bean: G1 Young Generation count=0 timeMs=0
|
||||||
|
bean: G1 Concurrent GC count=0 timeMs=0
|
||||||
|
bean: G1 Old Generation count=0 timeMs=0
|
||||||
|
generational? : yes (separate young + old cycles reported)
|
||||||
|
|
||||||
|
=== Memory pools ===
|
||||||
|
CodeHeap 'non-nmethods' type=Non-heap memory max=5,832,704 bytes (6 MB)
|
||||||
|
Metaspace type=Non-heap memory max=unbounded
|
||||||
|
CodeHeap 'profiled nmethods' type=Non-heap memory max=122,880,000 bytes (117 MB)
|
||||||
|
Compressed Class Space type=Non-heap memory max=1,073,741,824 bytes (1024 MB)
|
||||||
|
G1 Eden Space type=Heap memory max=unbounded
|
||||||
|
G1 Old Gen type=Heap memory max=2,147,483,648 bytes (2048 MB)
|
||||||
|
G1 Survivor Space type=Heap memory max=unbounded
|
||||||
|
CodeHeap 'non-profiled nmethods' type=Non-heap memory max=122,945,536 bytes (117 MB)
|
||||||
|
|
||||||
|
=== JVM arguments actually in effect ===
|
||||||
|
-XX:+UseG1GC
|
||||||
|
-Xms2g
|
||||||
|
-Xmx2g
|
||||||
|
--add-modules=ALL-DEFAULT
|
||||||
|
|
||||||
|
Tip: dump every ergonomic value the JVM chose with
|
||||||
|
java -XX:+UseZGC -Xms2g -Xmx2g -XX:+PrintFlagsFinal -version
|
||||||
34
results/01-env-zgc.txt
Normal file
34
results/01-env-zgc.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
=== Runtime ===
|
||||||
|
java.version : 25.0.3
|
||||||
|
java.vm.name : Java HotSpot(TM) 64-Bit Server VM
|
||||||
|
java.vm.version : 25.0.3+9-LTS-195
|
||||||
|
java.vendor : Oracle Corporation
|
||||||
|
os.name / os.arch : Windows 11 / amd64
|
||||||
|
availableProcessors : 12
|
||||||
|
maxMemory (-Xmx) : 2,147,483,648 bytes (2048 MB)
|
||||||
|
totalMemory (now) : 2,147,483,648 bytes (2048 MB)
|
||||||
|
|
||||||
|
=== Collector in use ===
|
||||||
|
bean: ZGC Minor Cycles count=0 timeMs=0
|
||||||
|
bean: ZGC Minor Pauses count=0 timeMs=0
|
||||||
|
bean: ZGC Major Cycles count=0 timeMs=0
|
||||||
|
bean: ZGC Major Pauses count=0 timeMs=0
|
||||||
|
generational? : yes (separate young + old cycles reported)
|
||||||
|
|
||||||
|
=== Memory pools ===
|
||||||
|
CodeHeap 'non-nmethods' type=Non-heap memory max=5,832,704 bytes (6 MB)
|
||||||
|
Metaspace type=Non-heap memory max=unbounded
|
||||||
|
CodeHeap 'profiled nmethods' type=Non-heap memory max=122,880,000 bytes (117 MB)
|
||||||
|
Compressed Class Space type=Non-heap memory max=1,073,741,824 bytes (1024 MB)
|
||||||
|
ZGC Old Generation type=Heap memory max=2,147,483,648 bytes (2048 MB)
|
||||||
|
ZGC Young Generation type=Heap memory max=2,147,483,648 bytes (2048 MB)
|
||||||
|
CodeHeap 'non-profiled nmethods' type=Non-heap memory max=122,945,536 bytes (117 MB)
|
||||||
|
|
||||||
|
=== JVM arguments actually in effect ===
|
||||||
|
-XX:+UseZGC
|
||||||
|
-Xms2g
|
||||||
|
-Xmx2g
|
||||||
|
--add-modules=ALL-DEFAULT
|
||||||
|
|
||||||
|
Tip: dump every ergonomic value the JVM chose with
|
||||||
|
java -XX:+UseZGC -Xms2g -Xmx2g -XX:+PrintFlagsFinal -version
|
||||||
513
results/02-flags-g1.txt
Normal file
513
results/02-flags-g1.txt
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
[Global flags]
|
||||||
|
ccstr AOTCache = {product} {default}
|
||||||
|
ccstr AOTCacheOutput = {product} {default}
|
||||||
|
bool AOTClassLinking = false {product} {default}
|
||||||
|
ccstr AOTConfiguration = {product} {default}
|
||||||
|
ccstr AOTMode = {product} {default}
|
||||||
|
int ActiveProcessorCount = -1 {product} {default}
|
||||||
|
uintx AdaptiveSizeDecrementScaleFactor = 4 {product} {default}
|
||||||
|
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
|
||||||
|
uintx AdaptiveSizePolicyInitializingSteps = 20 {product} {default}
|
||||||
|
uintx AdaptiveSizePolicyOutputInterval = 0 {product} {default}
|
||||||
|
uint AdaptiveSizePolicyWeight = 10 {product} {default}
|
||||||
|
uint AdaptiveSizeThroughPutPolicy = 0 {product} {default}
|
||||||
|
uint AdaptiveTimeWeight = 25 {product} {default}
|
||||||
|
bool AggressiveHeap = false {product} {default}
|
||||||
|
bool AlignVector = false {C2 product} {default}
|
||||||
|
ccstr AllocateHeapAt = {product} {default}
|
||||||
|
int AllocateInstancePrefetchLines = 1 {product} {default}
|
||||||
|
int AllocatePrefetchDistance = 256 {product} {default}
|
||||||
|
intx AllocatePrefetchInstr = 0 {product} {default}
|
||||||
|
int AllocatePrefetchLines = 3 {product} {default}
|
||||||
|
int AllocatePrefetchStepSize = 64 {product} {default}
|
||||||
|
int AllocatePrefetchStyle = 1 {product} {default}
|
||||||
|
bool AllowParallelDefineClass = false {product} {default}
|
||||||
|
bool AllowRedefinitionToAddDeleteMethods = false {product} {default}
|
||||||
|
bool AllowUserSignalHandlers = false {product} {default}
|
||||||
|
bool AllowVectorizeOnDemand = true {C2 product} {default}
|
||||||
|
bool AlwaysActAsServerClassMachine = false {product} {default}
|
||||||
|
bool AlwaysCompileLoopMethods = false {product} {default}
|
||||||
|
bool AlwaysPreTouch = false {product} {default}
|
||||||
|
bool AlwaysRestoreFPU = false {product} {default}
|
||||||
|
bool AlwaysTenure = false {product} {default}
|
||||||
|
ccstr ArchiveClassesAtExit = {product} {default}
|
||||||
|
intx ArrayCopyLoadStoreMaxElem = 8 {C2 product} {default}
|
||||||
|
size_t AsyncLogBufferSize = 2097152 {product} {default}
|
||||||
|
intx AutoBoxCacheMax = 128 {C2 product} {default}
|
||||||
|
bool AutoCreateSharedArchive = false {product} {default}
|
||||||
|
intx BCEATraceLevel = 0 {product} {default}
|
||||||
|
bool BackgroundCompilation = true {pd product} {default}
|
||||||
|
bool BlockLayoutByFrequency = true {C2 product} {default}
|
||||||
|
intx BlockLayoutMinDiamondPercentage = 20 {C2 product} {default}
|
||||||
|
bool BlockLayoutRotateLoops = true {C2 product} {default}
|
||||||
|
intx C1InlineStackLimit = 5 {C1 product} {default}
|
||||||
|
intx C1MaxInlineLevel = 9 {C1 product} {default}
|
||||||
|
intx C1MaxInlineSize = 35 {C1 product} {default}
|
||||||
|
intx C1MaxRecursiveInlineLevel = 1 {C1 product} {default}
|
||||||
|
intx C1MaxTrivialSize = 6 {C1 product} {default}
|
||||||
|
bool C1OptimizeVirtualCallProfiling = true {C1 product} {default}
|
||||||
|
bool C1ProfileBranches = true {C1 product} {default}
|
||||||
|
bool C1ProfileCalls = true {C1 product} {default}
|
||||||
|
bool C1ProfileCheckcasts = true {C1 product} {default}
|
||||||
|
bool C1ProfileInlinedCalls = true {C1 product} {default}
|
||||||
|
bool C1ProfileVirtualCalls = true {C1 product} {default}
|
||||||
|
bool C1UpdateMethodData = true {C1 product} {default}
|
||||||
|
intx CICompilerCount = 4 {product} {ergonomic}
|
||||||
|
bool CICompilerCountPerCPU = true {product} {default}
|
||||||
|
bool CITime = false {product} {default}
|
||||||
|
bool CheckJNICalls = false {product} {default}
|
||||||
|
bool ClassUnloading = true {product} {default}
|
||||||
|
bool ClassUnloadingWithConcurrentMark = true {product} {default}
|
||||||
|
bool ClipInlining = true {product} {default}
|
||||||
|
uintx CodeCacheExpansionSize = 65536 {pd product} {default}
|
||||||
|
bool CompactStrings = true {pd product} {default}
|
||||||
|
ccstr CompilationMode = default {product} {default}
|
||||||
|
ccstrlist CompileCommand = {product} {default}
|
||||||
|
ccstr CompileCommandFile = {product} {default}
|
||||||
|
ccstrlist CompileOnly = {product} {default}
|
||||||
|
intx CompileThreshold = 10000 {pd product} {default}
|
||||||
|
double CompileThresholdScaling = 1.000000 {product} {default}
|
||||||
|
int CompilerThreadPriority = -1 {product} {default}
|
||||||
|
intx CompilerThreadStackSize = 0 {pd product} {default}
|
||||||
|
size_t CompressedClassSpaceSize = 1073741824 {product} {default}
|
||||||
|
uint ConcGCThreads = 3 {product} {ergonomic}
|
||||||
|
intx ConditionalMoveLimit = 3 {C2 pd product} {default}
|
||||||
|
int ContendedPaddingWidth = 128 {product} {default}
|
||||||
|
bool CrashOnOutOfMemoryError = false {product} {default}
|
||||||
|
bool CreateCoredumpOnCrash = true {product} {default}
|
||||||
|
bool DTraceAllocProbes = false {product} {default}
|
||||||
|
bool DTraceMethodProbes = false {product} {default}
|
||||||
|
bool DTraceMonitorProbes = false {product} {default}
|
||||||
|
bool DisableAttachMechanism = false {product} {default}
|
||||||
|
bool DisableExplicitGC = false {product} {default}
|
||||||
|
bool DisplayVMOutputToStderr = false {product} {default}
|
||||||
|
bool DisplayVMOutputToStdout = false {product} {default}
|
||||||
|
bool DoEscapeAnalysis = true {C2 product} {default}
|
||||||
|
bool DontCompileHugeMethods = true {product} {default}
|
||||||
|
ccstr DumpLoadedClassList = {product} {default}
|
||||||
|
bool DumpReplayDataOnError = true {product} {default}
|
||||||
|
bool EagerXrunInit = false {product} {default}
|
||||||
|
intx EliminateAllocationArraySizeLimit = 64 {C2 product} {default}
|
||||||
|
bool EliminateAllocations = true {C2 product} {default}
|
||||||
|
bool EliminateAutoBox = true {C2 product} {default}
|
||||||
|
bool EliminateLocks = true {C2 product} {default}
|
||||||
|
bool EliminateNestedLocks = true {C2 product} {default}
|
||||||
|
bool EnableAllLargePageSizesForWindows = false {product} {default}
|
||||||
|
bool EnableContended = true {product} {default}
|
||||||
|
bool EnableDynamicAgentLoading = true {product} {default}
|
||||||
|
size_t ErgoHeapSizeLimit = 0 {product} {default}
|
||||||
|
ccstr ErrorFile = {product} {default}
|
||||||
|
bool ErrorFileToStderr = false {product} {default}
|
||||||
|
bool ErrorFileToStdout = false {product} {default}
|
||||||
|
uint64_t ErrorLogTimeout = 120 {product} {default}
|
||||||
|
double EscapeAnalysisTimeout = 20.000000 {C2 product} {default}
|
||||||
|
bool EstimateArgEscape = true {product} {default}
|
||||||
|
bool ExecutingUnitTests = false {product} {default}
|
||||||
|
bool ExitOnOutOfMemoryError = false {product} {default}
|
||||||
|
bool ExplicitGCInvokesConcurrent = false {product} {default}
|
||||||
|
bool ExtensiveErrorReports = false {product} {default}
|
||||||
|
ccstr ExtraSharedClassListFile = {product} {default}
|
||||||
|
bool FlightRecorder = false {product} {default}
|
||||||
|
ccstr FlightRecorderOptions = {product} {default}
|
||||||
|
bool ForceTimeHighResolution = false {product} {default}
|
||||||
|
intx FreqInlineSize = 325 {C2 pd product} {default}
|
||||||
|
uint FullGCHeapDumpLimit = 0 {manageable} {default}
|
||||||
|
double G1ConcMarkStepDurationMillis = 10.000000 {product} {default}
|
||||||
|
uint G1ConcRefinementThreads = 10 {product} {ergonomic}
|
||||||
|
uint G1ConfidencePercent = 50 {product} {default}
|
||||||
|
size_t G1HeapRegionSize = 1048576 {product} {ergonomic}
|
||||||
|
uint G1HeapWastePercent = 5 {product} {default}
|
||||||
|
uintx G1MixedGCCountTarget = 8 {product} {default}
|
||||||
|
uintx G1PeriodicGCInterval = 0 {manageable} {default}
|
||||||
|
bool G1PeriodicGCInvokesConcurrent = true {product} {default}
|
||||||
|
double G1PeriodicGCSystemLoadThreshold = 0.000000 {manageable} {default}
|
||||||
|
uint G1RSetUpdatingPauseTimePercent = 10 {product} {default}
|
||||||
|
uint G1RefProcDrainInterval = 1000 {product} {default}
|
||||||
|
uint G1ReservePercent = 10 {product} {default}
|
||||||
|
uint G1SATBBufferEnqueueingThresholdPercent = 60 {product} {default}
|
||||||
|
size_t G1SATBBufferSize = 1024 {product} {default}
|
||||||
|
size_t G1UpdateBufferSize = 256 {product} {default}
|
||||||
|
bool G1UseAdaptiveIHOP = true {product} {default}
|
||||||
|
uint GCCardSizeInBytes = 512 {product} {default}
|
||||||
|
uint GCDrainStackTargetSize = 64 {product} {default}
|
||||||
|
uint GCHeapFreeLimit = 2 {product} {default}
|
||||||
|
uintx GCPauseIntervalMillis = 201 {product} {default}
|
||||||
|
uint GCTimeLimit = 98 {product} {default}
|
||||||
|
uint GCTimeRatio = 12 {product} {default}
|
||||||
|
size_t HeapBaseMinAddress = 2147483648 {pd product} {default}
|
||||||
|
bool HeapDumpAfterFullGC = false {manageable} {default}
|
||||||
|
bool HeapDumpBeforeFullGC = false {manageable} {default}
|
||||||
|
int HeapDumpGzipLevel = 0 {manageable} {default}
|
||||||
|
bool HeapDumpOnOutOfMemoryError = false {manageable} {default}
|
||||||
|
ccstr HeapDumpPath = {manageable} {default}
|
||||||
|
uintx HeapMaximumCompactionInterval = 20 {product} {default}
|
||||||
|
uintx HeapSearchSteps = 3 {product} {default}
|
||||||
|
size_t HeapSizePerGCThread = 43620760 {product} {default}
|
||||||
|
bool IgnoreEmptyClassPaths = false {product} {default}
|
||||||
|
bool IgnoreUnrecognizedVMOptions = false {product} {default}
|
||||||
|
uintx IncreaseFirstTierCompileThresholdAt = 50 {product} {default}
|
||||||
|
bool IncrementalInline = true {C2 product} {default}
|
||||||
|
uintx InitialCodeCacheSize = 2555904 {pd product} {default}
|
||||||
|
size_t InitialHeapSize = 2147483648 {product} {command line}
|
||||||
|
double InitialRAMPercentage = 1.562500 {product} {default}
|
||||||
|
uintx InitialSurvivorRatio = 8 {product} {default}
|
||||||
|
uint InitialTenuringThreshold = 7 {product} {default}
|
||||||
|
uint InitiatingHeapOccupancyPercent = 45 {product} {default}
|
||||||
|
bool Inline = true {product} {default}
|
||||||
|
ccstr InlineDataFile = {product} {default}
|
||||||
|
intx InlineSmallCode = 2500 {C2 pd product} {default}
|
||||||
|
bool InlineSynchronizedMethods = true {C1 product} {default}
|
||||||
|
intx InteriorEntryAlignment = 16 {C2 pd product} {default}
|
||||||
|
intx InterpreterProfilePercentage = 33 {product} {default}
|
||||||
|
bool JavaMonitorsInStackTrace = true {product} {default}
|
||||||
|
int JavaPriority10_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority1_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority2_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority3_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority4_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority5_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority6_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority7_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority8_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority9_To_OSPriority = -1 {product} {default}
|
||||||
|
size_t LargePageHeapSizeThreshold = 134217728 {product} {default}
|
||||||
|
size_t LargePageSizeInBytes = 0 {product} {default}
|
||||||
|
intx LiveNodeCountInliningCutoff = 40000 {C2 product} {default}
|
||||||
|
int LockingMode = 2 {product} {default}
|
||||||
|
ccstr LogClassLoadingCauseFor = {product} {default}
|
||||||
|
intx LoopMaxUnroll = 16 {C2 product} {default}
|
||||||
|
intx LoopOptsCount = 43 {C2 product} {default}
|
||||||
|
intx LoopPercentProfileLimit = 10 {C2 pd product} {default}
|
||||||
|
uintx LoopStripMiningIter = 1000 {C2 product} {default}
|
||||||
|
uintx LoopStripMiningIterShortLoop = 100 {C2 product} {default}
|
||||||
|
intx LoopUnrollLimit = 60 {C2 pd product} {default}
|
||||||
|
intx LoopUnrollMin = 4 {C2 product} {default}
|
||||||
|
bool LoopUnswitching = true {C2 product} {default}
|
||||||
|
bool ManagementServer = false {product} {default}
|
||||||
|
size_t MarkStackSize = 4194304 {product} {ergonomic}
|
||||||
|
size_t MarkStackSizeMax = 536870912 {product} {ergonomic}
|
||||||
|
uint MarkSweepAlwaysCompactCount = 4 {product} {default}
|
||||||
|
uint MarkSweepDeadRatio = 5 {product} {default}
|
||||||
|
intx MaxBCEAEstimateLevel = 5 {product} {default}
|
||||||
|
intx MaxBCEAEstimateSize = 150 {product} {default}
|
||||||
|
uint64_t MaxDirectMemorySize = 0 {product} {default}
|
||||||
|
bool MaxFDLimit = true {product} {default}
|
||||||
|
uintx MaxGCPauseMillis = 200 {product} {default}
|
||||||
|
uintx MaxHeapFreeRatio = 70 {manageable} {default}
|
||||||
|
size_t MaxHeapSize = 2147483648 {product} {command line}
|
||||||
|
intx MaxInlineLevel = 15 {C2 product} {default}
|
||||||
|
intx MaxInlineSize = 35 {C2 product} {default}
|
||||||
|
intx MaxJNILocalCapacity = 65536 {product} {default}
|
||||||
|
int MaxJavaStackTraceDepth = 1024 {product} {default}
|
||||||
|
intx MaxJumpTableSize = 65000 {C2 product} {default}
|
||||||
|
intx MaxJumpTableSparseness = 5 {C2 product} {default}
|
||||||
|
intx MaxLabelRootDepth = 1100 {C2 product} {default}
|
||||||
|
intx MaxLoopPad = 15 {C2 product} {default}
|
||||||
|
size_t MaxMetaspaceExpansion = 5439488 {product} {default}
|
||||||
|
uint MaxMetaspaceFreeRatio = 70 {product} {default}
|
||||||
|
size_t MaxMetaspaceSize = 18446744073709551615 {product} {default}
|
||||||
|
size_t MaxNewSize = 1287651328 {product} {ergonomic}
|
||||||
|
intx MaxNodeLimit = 80000 {C2 product} {default}
|
||||||
|
uint64_t MaxRAM = 137438953472 {pd product} {default}
|
||||||
|
double MaxRAMPercentage = 25.000000 {product} {default}
|
||||||
|
intx MaxRecursiveInlineLevel = 1 {C2 product} {default}
|
||||||
|
uint MaxTenuringThreshold = 15 {product} {default}
|
||||||
|
intx MaxTrivialSize = 6 {C2 product} {default}
|
||||||
|
intx MaxVectorSize = 32 {C2 product} {default}
|
||||||
|
size_t MetaspaceSize = 22020096 {product} {default}
|
||||||
|
bool MethodFlushing = true {product} {default}
|
||||||
|
size_t MinHeapDeltaBytes = 1048576 {product} {ergonomic}
|
||||||
|
uintx MinHeapFreeRatio = 40 {manageable} {default}
|
||||||
|
size_t MinHeapSize = 2147483648 {product} {command line}
|
||||||
|
intx MinJumpTableSize = 10 {C2 pd product} {default}
|
||||||
|
size_t MinMetaspaceExpansion = 327680 {product} {default}
|
||||||
|
uint MinMetaspaceFreeRatio = 40 {product} {default}
|
||||||
|
double MinRAMPercentage = 50.000000 {product} {default}
|
||||||
|
uintx MinSurvivorRatio = 3 {product} {default}
|
||||||
|
size_t MinTLABSize = 2048 {product} {default}
|
||||||
|
intx MultiArrayExpandLimit = 6 {C2 product} {default}
|
||||||
|
uintx NUMAChunkResizeWeight = 20 {product} {default}
|
||||||
|
size_t NUMAInterleaveGranularity = 2097152 {product} {default}
|
||||||
|
size_t NUMASpaceResizeRate = 1073741824 {product} {default}
|
||||||
|
bool NUMAStats = false {product} {default}
|
||||||
|
ccstr NativeMemoryTracking = off {product} {default}
|
||||||
|
bool NeverActAsServerClassMachine = false {pd product} {default}
|
||||||
|
bool NeverTenure = false {product} {default}
|
||||||
|
uintx NewRatio = 2 {product} {default}
|
||||||
|
size_t NewSize = 1363144 {product} {default}
|
||||||
|
size_t NewSizeThreadIncrease = 5320 {pd product} {default}
|
||||||
|
intx NmethodSweepActivity = 4 {product} {default}
|
||||||
|
intx NodeLimitFudgeFactor = 2000 {C2 product} {default}
|
||||||
|
uintx NonNMethodCodeHeapSize = 5832704 {pd product} {ergonomic}
|
||||||
|
uintx NonProfiledCodeHeapSize = 122945536 {pd product} {ergonomic}
|
||||||
|
intx NumberOfLoopInstrToAlign = 4 {C2 product} {default}
|
||||||
|
int ObjectAlignmentInBytes = 8 {product lp64_product} {default}
|
||||||
|
size_t OldPLABSize = 1024 {product} {default}
|
||||||
|
bool OmitStackTraceInFastThrow = true {product} {default}
|
||||||
|
ccstrlist OnError = {product} {default}
|
||||||
|
ccstrlist OnOutOfMemoryError = {product} {default}
|
||||||
|
intx OnStackReplacePercentage = 140 {pd product} {default}
|
||||||
|
bool OptimizeFill = false {C2 product} {default}
|
||||||
|
bool OptimizePtrCompare = true {C2 product} {default}
|
||||||
|
bool OptimizeStringConcat = true {C2 product} {default}
|
||||||
|
bool OptoBundling = false {C2 pd product} {default}
|
||||||
|
intx OptoLoopAlignment = 16 {pd product} {default}
|
||||||
|
bool OptoRegScheduling = true {C2 pd product} {default}
|
||||||
|
bool OptoScheduling = false {C2 pd product} {default}
|
||||||
|
uint PLABWeight = 75 {product} {default}
|
||||||
|
bool PSChunkLargeArrays = true {product} {default}
|
||||||
|
int ParGCArrayScanChunk = 50 {product} {default}
|
||||||
|
uint ParallelGCBufferWastePct = 10 {product} {default}
|
||||||
|
uint ParallelGCThreads = 10 {product} {default}
|
||||||
|
bool ParallelRefProcBalancingEnabled = true {product} {default}
|
||||||
|
bool ParallelRefProcEnabled = true {product} {default}
|
||||||
|
bool PartialPeelAtUnsignedTests = true {C2 product} {default}
|
||||||
|
bool PartialPeelLoop = true {C2 product} {default}
|
||||||
|
intx PartialPeelNewPhiDelta = 0 {C2 product} {default}
|
||||||
|
uint PausePadding = 1 {product} {default}
|
||||||
|
intx PerBytecodeRecompilationCutoff = 200 {product} {default}
|
||||||
|
intx PerBytecodeTrapLimit = 4 {product} {default}
|
||||||
|
intx PerMethodRecompilationCutoff = 400 {product} {default}
|
||||||
|
intx PerMethodTrapLimit = 100 {product} {default}
|
||||||
|
bool PerfAllowAtExitRegistration = false {product} {default}
|
||||||
|
bool PerfBypassFileSystemCheck = false {product} {default}
|
||||||
|
int PerfDataMemorySize = 32768 {product} {default}
|
||||||
|
ccstr PerfDataSaveFile = {product} {default}
|
||||||
|
bool PerfDataSaveToFile = false {product} {default}
|
||||||
|
bool PerfDisableSharedMem = false {product} {default}
|
||||||
|
int PerfMaxStringConstLength = 1024 {product} {default}
|
||||||
|
size_t PreTouchParallelChunkSize = 1073741824 {pd product} {default}
|
||||||
|
bool PreferInterpreterNativeStubs = false {pd product} {default}
|
||||||
|
intx PrefetchCopyIntervalInBytes = 576 {product} {default}
|
||||||
|
intx PrefetchScanIntervalInBytes = 576 {product} {default}
|
||||||
|
bool PreserveFramePointer = false {pd product} {default}
|
||||||
|
size_t PretenureSizeThreshold = 0 {product} {default}
|
||||||
|
bool PrintClassHistogram = false {manageable} {default}
|
||||||
|
bool PrintCodeCache = false {product} {default}
|
||||||
|
bool PrintCodeCacheOnCompilation = false {product} {default}
|
||||||
|
bool PrintCommandLineFlags = false {product} {default}
|
||||||
|
bool PrintCompilation = false {product} {default}
|
||||||
|
bool PrintConcurrentLocks = false {manageable} {default}
|
||||||
|
bool PrintExtendedThreadInfo = false {product} {default}
|
||||||
|
bool PrintFlagsFinal = true {product} {command line}
|
||||||
|
bool PrintFlagsInitial = false {product} {default}
|
||||||
|
bool PrintFlagsRanges = false {product} {default}
|
||||||
|
bool PrintGC = false {product} {default}
|
||||||
|
bool PrintGCDetails = false {product} {default}
|
||||||
|
bool PrintHeapAtSIGBREAK = true {product} {default}
|
||||||
|
bool PrintSharedArchiveAndExit = false {product} {default}
|
||||||
|
bool PrintStringTableStatistics = false {product} {default}
|
||||||
|
bool PrintTieredEvents = false {product} {default}
|
||||||
|
bool PrintVMOptions = false {product} {default}
|
||||||
|
bool PrintWarnings = true {product} {default}
|
||||||
|
bool ProfileExceptionHandlers = true {product} {default}
|
||||||
|
bool ProfileInterpreter = true {pd product} {default}
|
||||||
|
intx ProfileMaturityPercentage = 20 {product} {default}
|
||||||
|
uintx ProfiledCodeHeapSize = 122880000 {pd product} {ergonomic}
|
||||||
|
uint PromotedPadding = 3 {product} {default}
|
||||||
|
uintx QueuedAllocationWarningCount = 0 {product} {default}
|
||||||
|
bool RangeCheckElimination = true {product} {default}
|
||||||
|
bool ReassociateInvariants = true {C2 product} {default}
|
||||||
|
bool RecordDynamicDumpInfo = false {product} {default}
|
||||||
|
bool ReduceBulkZeroing = true {C2 product} {default}
|
||||||
|
bool ReduceFieldZeroing = true {C2 product} {default}
|
||||||
|
bool ReduceInitialCardMarks = true {C2 product} {default}
|
||||||
|
bool ReduceSignalUsage = false {product} {default}
|
||||||
|
bool RelaxAccessControlCheck = false {product} {default}
|
||||||
|
ccstr ReplayDataFile = {product} {default}
|
||||||
|
uintx ReservedCodeCacheSize = 251658240 {pd product} {ergonomic}
|
||||||
|
bool ResizePLAB = true {product} {default}
|
||||||
|
bool ResizeTLAB = true {product} {default}
|
||||||
|
bool RestoreMXCSROnJNICalls = false {product} {default}
|
||||||
|
bool RestrictContended = true {product} {default}
|
||||||
|
bool RestrictReservedStack = true {product} {default}
|
||||||
|
bool RewriteBytecodes = true {pd product} {default}
|
||||||
|
bool RewriteFrequentPairs = true {pd product} {default}
|
||||||
|
bool SafepointTimeout = false {product} {default}
|
||||||
|
double SafepointTimeoutDelay = 10000.000000 {product} {default}
|
||||||
|
bool SegmentedCodeCache = true {product} {ergonomic}
|
||||||
|
double SelfDestructTimer = 0.000000 {product} {default}
|
||||||
|
ccstr SharedArchiveConfigFile = {product} {default}
|
||||||
|
ccstr SharedArchiveFile = {product} {default}
|
||||||
|
size_t SharedBaseAddress = 100663296 {product} {default}
|
||||||
|
ccstr SharedClassListFile = {product} {default}
|
||||||
|
uint SharedSymbolTableBucketSize = 4 {product} {default}
|
||||||
|
bool ShowCodeDetailsInExceptionMessages = true {manageable} {default}
|
||||||
|
bool ShowMessageBoxOnError = false {product} {default}
|
||||||
|
bool ShrinkHeapInSteps = true {product} {default}
|
||||||
|
size_t SoftMaxHeapSize = 2147483648 {manageable} {ergonomic}
|
||||||
|
intx SoftRefLRUPolicyMSPerMB = 1000 {product} {default}
|
||||||
|
bool SplitIfBlocks = true {C2 product} {default}
|
||||||
|
intx StackRedPages = 1 {pd product} {default}
|
||||||
|
intx StackReservedPages = 0 {pd product} {default}
|
||||||
|
intx StackShadowPages = 8 {pd product} {default}
|
||||||
|
bool StackTraceInThrowable = true {product} {default}
|
||||||
|
intx StackYellowPages = 3 {pd product} {default}
|
||||||
|
uintx StartAggressiveSweepingAt = 10 {product} {default}
|
||||||
|
bool StartAttachListener = false {product} {default}
|
||||||
|
ccstr StartFlightRecording = {product} {default}
|
||||||
|
uint StringDeduplicationAgeThreshold = 3 {product} {default}
|
||||||
|
uintx StringTableSize = 65536 {product} {default}
|
||||||
|
bool SuperWordLoopUnrollAnalysis = true {C2 pd product} {default}
|
||||||
|
bool SuperWordReductions = true {C2 product} {default}
|
||||||
|
bool SuppressFatalErrorMessage = false {product} {default}
|
||||||
|
uint SurvivorPadding = 3 {product} {default}
|
||||||
|
uintx SurvivorRatio = 8 {product} {default}
|
||||||
|
double SweeperThreshold = 15.000000 {product} {default}
|
||||||
|
uintx TLABAllocationWeight = 35 {product} {default}
|
||||||
|
uintx TLABRefillWasteFraction = 64 {product} {default}
|
||||||
|
size_t TLABSize = 0 {product} {default}
|
||||||
|
uintx TLABWasteIncrement = 4 {product} {default}
|
||||||
|
uintx TLABWasteTargetPercent = 1 {product} {default}
|
||||||
|
uint TargetPLABWastePct = 10 {product} {default}
|
||||||
|
uint TargetSurvivorRatio = 50 {product} {default}
|
||||||
|
uint TenuredGenerationSizeIncrement = 20 {product} {default}
|
||||||
|
uint TenuredGenerationSizeSupplement = 80 {product} {default}
|
||||||
|
uintx TenuredGenerationSizeSupplementDecay = 2 {product} {default}
|
||||||
|
int ThreadPriorityPolicy = 0 {product} {default}
|
||||||
|
bool ThreadPriorityVerbose = false {product} {default}
|
||||||
|
intx ThreadStackSize = 0 {pd product} {default}
|
||||||
|
uint ThresholdTolerance = 10 {product} {default}
|
||||||
|
intx Tier0BackedgeNotifyFreqLog = 10 {product} {default}
|
||||||
|
intx Tier0InvokeNotifyFreqLog = 7 {product} {default}
|
||||||
|
intx Tier0ProfilingStartPercentage = 200 {product} {default}
|
||||||
|
intx Tier23InlineeNotifyFreqLog = 20 {product} {default}
|
||||||
|
intx Tier2BackEdgeThreshold = 0 {product} {default}
|
||||||
|
intx Tier2BackedgeNotifyFreqLog = 14 {product} {default}
|
||||||
|
intx Tier2CompileThreshold = 0 {product} {default}
|
||||||
|
intx Tier2InvokeNotifyFreqLog = 11 {product} {default}
|
||||||
|
intx Tier3BackEdgeThreshold = 60000 {product} {default}
|
||||||
|
intx Tier3BackedgeNotifyFreqLog = 13 {product} {default}
|
||||||
|
intx Tier3CompileThreshold = 2000 {product} {default}
|
||||||
|
intx Tier3DelayOff = 2 {product} {default}
|
||||||
|
intx Tier3DelayOn = 5 {product} {default}
|
||||||
|
intx Tier3InvocationThreshold = 200 {product} {default}
|
||||||
|
intx Tier3InvokeNotifyFreqLog = 10 {product} {default}
|
||||||
|
intx Tier3LoadFeedback = 5 {product} {default}
|
||||||
|
intx Tier3MinInvocationThreshold = 100 {product} {default}
|
||||||
|
intx Tier4BackEdgeThreshold = 40000 {product} {default}
|
||||||
|
intx Tier4CompileThreshold = 15000 {product} {default}
|
||||||
|
intx Tier4InvocationThreshold = 5000 {product} {default}
|
||||||
|
intx Tier4LoadFeedback = 3 {product} {default}
|
||||||
|
intx Tier4MinInvocationThreshold = 600 {product} {default}
|
||||||
|
bool TieredCompilation = true {pd product} {default}
|
||||||
|
intx TieredCompileTaskTimeout = 50 {product} {default}
|
||||||
|
intx TieredRateUpdateMaxTime = 25 {product} {default}
|
||||||
|
intx TieredRateUpdateMinTime = 1 {product} {default}
|
||||||
|
intx TieredStopAtLevel = 4 {product} {default}
|
||||||
|
ccstr TraceJVMTI = {product} {default}
|
||||||
|
intx TrackedInitializationLimit = 50 {C2 product} {default}
|
||||||
|
bool TrapBasedNullChecks = false {pd product} {default}
|
||||||
|
bool TrapBasedRangeChecks = false {C2 pd product} {default}
|
||||||
|
uint TrimNativeHeapInterval = 0 {product} {default}
|
||||||
|
int TypeProfileArgsLimit = 2 {product} {default}
|
||||||
|
uint TypeProfileLevel = 111 {pd product} {default}
|
||||||
|
intx TypeProfileMajorReceiverPercent = 90 {C2 product} {default}
|
||||||
|
int TypeProfileParmsLimit = 2 {product} {default}
|
||||||
|
intx TypeProfileSubTypeCheckCommonThreshold = 50 {C2 product} {default}
|
||||||
|
intx TypeProfileWidth = 2 {product} {default}
|
||||||
|
int UnguardOnExecutionViolation = 0 {product} {default}
|
||||||
|
bool UseAES = true {product} {default}
|
||||||
|
int UseAVX = 2 {ARCH product} {default}
|
||||||
|
bool UseAdaptiveGenerationSizePolicyAtMajorCollection = true {product} {default}
|
||||||
|
bool UseAdaptiveGenerationSizePolicyAtMinorCollection = true {product} {default}
|
||||||
|
bool UseAdaptiveNUMAChunkSizing = true {product} {default}
|
||||||
|
bool UseAdaptiveSizeDecayMajorGCCost = true {product} {default}
|
||||||
|
bool UseAdaptiveSizePolicy = true {product} {default}
|
||||||
|
bool UseAdaptiveSizePolicyFootprintGoal = true {product} {default}
|
||||||
|
bool UseAdaptiveSizePolicyWithSystemGC = false {product} {default}
|
||||||
|
bool UseAddressNop = true {ARCH product} {default}
|
||||||
|
bool UseAllWindowsProcessorGroups = false {product} {default}
|
||||||
|
bool UseBASE64Intrinsics = true {product} {default}
|
||||||
|
bool UseBMI1Instructions = true {ARCH product} {default}
|
||||||
|
bool UseBMI2Instructions = true {ARCH product} {default}
|
||||||
|
bool UseBimorphicInlining = true {C2 product} {default}
|
||||||
|
bool UseCLMUL = true {ARCH product} {default}
|
||||||
|
bool UseCMoveUnconditionally = false {C2 product} {default}
|
||||||
|
bool UseCodeCacheFlushing = true {product} {default}
|
||||||
|
bool UseCompactObjectHeaders = false {product lp64_product} {default}
|
||||||
|
bool UseCompiler = true {product} {default}
|
||||||
|
bool UseCompressedClassPointers = true {product lp64_product} {default}
|
||||||
|
bool UseCompressedOops = true {product lp64_product} {ergonomic}
|
||||||
|
bool UseCondCardMark = false {product} {default}
|
||||||
|
bool UseCountLeadingZerosInstruction = true {ARCH product} {default}
|
||||||
|
bool UseCountTrailingZerosInstruction = true {ARCH product} {default}
|
||||||
|
bool UseCountedLoopSafepoints = true {C2 product} {default}
|
||||||
|
bool UseDivMod = true {C2 product} {default}
|
||||||
|
bool UseDynamicNumberOfCompilerThreads = true {product} {default}
|
||||||
|
bool UseDynamicNumberOfGCThreads = true {product} {default}
|
||||||
|
bool UseFMA = true {product} {default}
|
||||||
|
bool UseFPUForSpilling = true {C2 product} {default}
|
||||||
|
bool UseFastJNIAccessors = true {product} {default}
|
||||||
|
bool UseFastStosb = false {ARCH product} {default}
|
||||||
|
bool UseG1GC = true {product} {command line}
|
||||||
|
bool UseGCOverheadLimit = true {product} {default}
|
||||||
|
bool UseInlineCaches = true {product} {default}
|
||||||
|
bool UseInterpreter = true {product} {default}
|
||||||
|
bool UseJumpTables = true {C2 product} {default}
|
||||||
|
bool UseLargePages = false {pd product} {default}
|
||||||
|
bool UseLargePagesIndividualAllocation = false {pd product} {ergonomic}
|
||||||
|
bool UseLoopCounter = true {product} {default}
|
||||||
|
bool UseLoopInvariantCodeMotion = true {C1 product} {default}
|
||||||
|
bool UseLoopPredicate = true {C2 product} {default}
|
||||||
|
bool UseMaximumCompactionOnSystemGC = true {product} {default}
|
||||||
|
bool UseNUMA = false {product} {default}
|
||||||
|
bool UseNUMAInterleaving = false {product} {default}
|
||||||
|
bool UseNewLongLShift = true {ARCH product} {default}
|
||||||
|
bool UseOSErrorReporting = false {product} {default}
|
||||||
|
bool UseOnStackReplacement = true {pd product} {default}
|
||||||
|
bool UseOnlyInlinedBimorphic = true {C2 product} {default}
|
||||||
|
bool UsePSAdaptiveSurvivorSizePolicy = true {product} {default}
|
||||||
|
bool UseParallelGC = false {product} {default}
|
||||||
|
bool UsePerfData = true {product} {default}
|
||||||
|
bool UsePopCountInstruction = true {product} {default}
|
||||||
|
bool UseProfiledLoopPredicate = true {C2 product} {default}
|
||||||
|
bool UseSHA = true {product} {default}
|
||||||
|
int UseSSE = 4 {ARCH product} {default}
|
||||||
|
bool UseSSE42Intrinsics = true {ARCH product} {default}
|
||||||
|
bool UseSerialGC = false {product} {default}
|
||||||
|
bool UseShenandoahGC = false {product} {default}
|
||||||
|
bool UseSignalChaining = true {product} {default}
|
||||||
|
bool UseStoreImmI16 = true {ARCH product} {default}
|
||||||
|
bool UseStringDeduplication = false {product} {default}
|
||||||
|
bool UseSubwordForMaxVector = true {C2 product} {default}
|
||||||
|
bool UseSuperWord = true {C2 product} {default}
|
||||||
|
bool UseSystemMemoryBarrier = false {product} {default}
|
||||||
|
bool UseTLAB = true {product} {default}
|
||||||
|
bool UseThreadPriorities = true {pd product} {default}
|
||||||
|
bool UseTypeProfile = true {product} {default}
|
||||||
|
bool UseTypeSpeculation = true {C2 product} {default}
|
||||||
|
bool UseUnalignedLoadStores = true {ARCH product} {default}
|
||||||
|
bool UseVectorCmov = false {C2 product} {default}
|
||||||
|
bool UseXMMForArrayCopy = true {product} {default}
|
||||||
|
bool UseXMMForObjInit = true {ARCH product} {default}
|
||||||
|
bool UseXmmI2D = true {ARCH product} {default}
|
||||||
|
bool UseXmmI2F = true {ARCH product} {default}
|
||||||
|
bool UseXmmLoadAndClearUpper = true {ARCH product} {default}
|
||||||
|
bool UseXmmRegToRegMoveAll = true {ARCH product} {default}
|
||||||
|
bool UseZGC = false {product} {default}
|
||||||
|
intx UserThreadWaitAttemptsAtExit = 30 {product} {default}
|
||||||
|
int VMThreadPriority = -1 {product} {default}
|
||||||
|
intx VMThreadStackSize = 0 {pd product} {default}
|
||||||
|
intx ValueMapInitialSize = 11 {C1 product} {default}
|
||||||
|
intx ValueMapMaxLoopSize = 8 {C1 product} {default}
|
||||||
|
intx ValueSearchLimit = 1000 {C2 product} {default}
|
||||||
|
bool VerifySharedSpaces = false {product} {default}
|
||||||
|
uint YoungGenerationSizeIncrement = 20 {product} {default}
|
||||||
|
uint YoungGenerationSizeSupplement = 80 {product} {default}
|
||||||
|
uintx YoungGenerationSizeSupplementDecay = 8 {product} {default}
|
||||||
|
size_t YoungPLABSize = 4096 {product} {default}
|
||||||
|
double ZAllocationSpikeTolerance = 2.000000 {product} {default}
|
||||||
|
double ZCollectionInterval = 0.000000 {product} {default}
|
||||||
|
double ZCollectionIntervalMajor = -1.000000 {product} {default}
|
||||||
|
double ZCollectionIntervalMinor = -1.000000 {product} {default}
|
||||||
|
bool ZCollectionIntervalOnly = false {product} {default}
|
||||||
|
double ZFragmentationLimit = 5.000000 {product} {default}
|
||||||
|
bool ZProactive = true {product} {default}
|
||||||
|
bool ZUncommit = true {product} {default}
|
||||||
|
uintx ZUncommitDelay = 300 {product} {default}
|
||||||
|
double ZYoungCompactionLimit = 25.000000 {product} {default}
|
||||||
|
bool ZeroTLAB = false {product} {default}
|
||||||
|
java version "25.0.3" 2026-04-21 LTS
|
||||||
|
Java(TM) SE Runtime Environment (build 25.0.3+9-LTS-195)
|
||||||
|
Java HotSpot(TM) 64-Bit Server VM (build 25.0.3+9-LTS-195, mixed mode, sharing)
|
||||||
513
results/02-flags-zgc.txt
Normal file
513
results/02-flags-zgc.txt
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
[Global flags]
|
||||||
|
ccstr AOTCache = {product} {default}
|
||||||
|
ccstr AOTCacheOutput = {product} {default}
|
||||||
|
bool AOTClassLinking = false {product} {default}
|
||||||
|
ccstr AOTConfiguration = {product} {default}
|
||||||
|
ccstr AOTMode = {product} {default}
|
||||||
|
int ActiveProcessorCount = -1 {product} {default}
|
||||||
|
uintx AdaptiveSizeDecrementScaleFactor = 4 {product} {default}
|
||||||
|
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
|
||||||
|
uintx AdaptiveSizePolicyInitializingSteps = 20 {product} {default}
|
||||||
|
uintx AdaptiveSizePolicyOutputInterval = 0 {product} {default}
|
||||||
|
uint AdaptiveSizePolicyWeight = 10 {product} {default}
|
||||||
|
uint AdaptiveSizeThroughPutPolicy = 0 {product} {default}
|
||||||
|
uint AdaptiveTimeWeight = 25 {product} {default}
|
||||||
|
bool AggressiveHeap = false {product} {default}
|
||||||
|
bool AlignVector = false {C2 product} {default}
|
||||||
|
ccstr AllocateHeapAt = {product} {default}
|
||||||
|
int AllocateInstancePrefetchLines = 1 {product} {default}
|
||||||
|
int AllocatePrefetchDistance = 256 {product} {default}
|
||||||
|
intx AllocatePrefetchInstr = 0 {product} {default}
|
||||||
|
int AllocatePrefetchLines = 3 {product} {default}
|
||||||
|
int AllocatePrefetchStepSize = 64 {product} {default}
|
||||||
|
int AllocatePrefetchStyle = 1 {product} {default}
|
||||||
|
bool AllowParallelDefineClass = false {product} {default}
|
||||||
|
bool AllowRedefinitionToAddDeleteMethods = false {product} {default}
|
||||||
|
bool AllowUserSignalHandlers = false {product} {default}
|
||||||
|
bool AllowVectorizeOnDemand = true {C2 product} {default}
|
||||||
|
bool AlwaysActAsServerClassMachine = false {product} {default}
|
||||||
|
bool AlwaysCompileLoopMethods = false {product} {default}
|
||||||
|
bool AlwaysPreTouch = false {product} {default}
|
||||||
|
bool AlwaysRestoreFPU = false {product} {default}
|
||||||
|
bool AlwaysTenure = false {product} {default}
|
||||||
|
ccstr ArchiveClassesAtExit = {product} {default}
|
||||||
|
intx ArrayCopyLoadStoreMaxElem = 8 {C2 product} {default}
|
||||||
|
size_t AsyncLogBufferSize = 2097152 {product} {default}
|
||||||
|
intx AutoBoxCacheMax = 128 {C2 product} {default}
|
||||||
|
bool AutoCreateSharedArchive = false {product} {default}
|
||||||
|
intx BCEATraceLevel = 0 {product} {default}
|
||||||
|
bool BackgroundCompilation = true {pd product} {default}
|
||||||
|
bool BlockLayoutByFrequency = true {C2 product} {default}
|
||||||
|
intx BlockLayoutMinDiamondPercentage = 20 {C2 product} {default}
|
||||||
|
bool BlockLayoutRotateLoops = true {C2 product} {default}
|
||||||
|
intx C1InlineStackLimit = 5 {C1 product} {default}
|
||||||
|
intx C1MaxInlineLevel = 9 {C1 product} {default}
|
||||||
|
intx C1MaxInlineSize = 35 {C1 product} {default}
|
||||||
|
intx C1MaxRecursiveInlineLevel = 1 {C1 product} {default}
|
||||||
|
intx C1MaxTrivialSize = 6 {C1 product} {default}
|
||||||
|
bool C1OptimizeVirtualCallProfiling = true {C1 product} {default}
|
||||||
|
bool C1ProfileBranches = true {C1 product} {default}
|
||||||
|
bool C1ProfileCalls = true {C1 product} {default}
|
||||||
|
bool C1ProfileCheckcasts = true {C1 product} {default}
|
||||||
|
bool C1ProfileInlinedCalls = true {C1 product} {default}
|
||||||
|
bool C1ProfileVirtualCalls = true {C1 product} {default}
|
||||||
|
bool C1UpdateMethodData = true {C1 product} {default}
|
||||||
|
intx CICompilerCount = 4 {product} {ergonomic}
|
||||||
|
bool CICompilerCountPerCPU = true {product} {default}
|
||||||
|
bool CITime = false {product} {default}
|
||||||
|
bool CheckJNICalls = false {product} {default}
|
||||||
|
bool ClassUnloading = true {product} {default}
|
||||||
|
bool ClassUnloadingWithConcurrentMark = true {product} {default}
|
||||||
|
bool ClipInlining = true {product} {default}
|
||||||
|
uintx CodeCacheExpansionSize = 65536 {pd product} {default}
|
||||||
|
bool CompactStrings = true {pd product} {default}
|
||||||
|
ccstr CompilationMode = default {product} {default}
|
||||||
|
ccstrlist CompileCommand = {product} {default}
|
||||||
|
ccstr CompileCommandFile = {product} {default}
|
||||||
|
ccstrlist CompileOnly = {product} {default}
|
||||||
|
intx CompileThreshold = 10000 {pd product} {default}
|
||||||
|
double CompileThresholdScaling = 1.000000 {product} {default}
|
||||||
|
int CompilerThreadPriority = -1 {product} {default}
|
||||||
|
intx CompilerThreadStackSize = 0 {pd product} {default}
|
||||||
|
size_t CompressedClassSpaceSize = 1073741824 {product} {default}
|
||||||
|
uint ConcGCThreads = 3 {product} {default}
|
||||||
|
intx ConditionalMoveLimit = 3 {C2 pd product} {default}
|
||||||
|
int ContendedPaddingWidth = 128 {product} {default}
|
||||||
|
bool CrashOnOutOfMemoryError = false {product} {default}
|
||||||
|
bool CreateCoredumpOnCrash = true {product} {default}
|
||||||
|
bool DTraceAllocProbes = false {product} {default}
|
||||||
|
bool DTraceMethodProbes = false {product} {default}
|
||||||
|
bool DTraceMonitorProbes = false {product} {default}
|
||||||
|
bool DisableAttachMechanism = false {product} {default}
|
||||||
|
bool DisableExplicitGC = false {product} {default}
|
||||||
|
bool DisplayVMOutputToStderr = false {product} {default}
|
||||||
|
bool DisplayVMOutputToStdout = false {product} {default}
|
||||||
|
bool DoEscapeAnalysis = true {C2 product} {default}
|
||||||
|
bool DontCompileHugeMethods = true {product} {default}
|
||||||
|
ccstr DumpLoadedClassList = {product} {default}
|
||||||
|
bool DumpReplayDataOnError = true {product} {default}
|
||||||
|
bool EagerXrunInit = false {product} {default}
|
||||||
|
intx EliminateAllocationArraySizeLimit = 64 {C2 product} {default}
|
||||||
|
bool EliminateAllocations = true {C2 product} {default}
|
||||||
|
bool EliminateAutoBox = true {C2 product} {default}
|
||||||
|
bool EliminateLocks = true {C2 product} {default}
|
||||||
|
bool EliminateNestedLocks = true {C2 product} {default}
|
||||||
|
bool EnableAllLargePageSizesForWindows = false {product} {default}
|
||||||
|
bool EnableContended = true {product} {default}
|
||||||
|
bool EnableDynamicAgentLoading = true {product} {default}
|
||||||
|
size_t ErgoHeapSizeLimit = 0 {product} {default}
|
||||||
|
ccstr ErrorFile = {product} {default}
|
||||||
|
bool ErrorFileToStderr = false {product} {default}
|
||||||
|
bool ErrorFileToStdout = false {product} {default}
|
||||||
|
uint64_t ErrorLogTimeout = 120 {product} {default}
|
||||||
|
double EscapeAnalysisTimeout = 20.000000 {C2 product} {default}
|
||||||
|
bool EstimateArgEscape = true {product} {default}
|
||||||
|
bool ExecutingUnitTests = false {product} {default}
|
||||||
|
bool ExitOnOutOfMemoryError = false {product} {default}
|
||||||
|
bool ExplicitGCInvokesConcurrent = false {product} {default}
|
||||||
|
bool ExtensiveErrorReports = false {product} {default}
|
||||||
|
ccstr ExtraSharedClassListFile = {product} {default}
|
||||||
|
bool FlightRecorder = false {product} {default}
|
||||||
|
ccstr FlightRecorderOptions = {product} {default}
|
||||||
|
bool ForceTimeHighResolution = false {product} {default}
|
||||||
|
intx FreqInlineSize = 325 {C2 pd product} {default}
|
||||||
|
uint FullGCHeapDumpLimit = 0 {manageable} {default}
|
||||||
|
double G1ConcMarkStepDurationMillis = 10.000000 {product} {default}
|
||||||
|
uint G1ConcRefinementThreads = 0 {product} {default}
|
||||||
|
uint G1ConfidencePercent = 50 {product} {default}
|
||||||
|
size_t G1HeapRegionSize = 0 {product} {default}
|
||||||
|
uint G1HeapWastePercent = 5 {product} {default}
|
||||||
|
uintx G1MixedGCCountTarget = 8 {product} {default}
|
||||||
|
uintx G1PeriodicGCInterval = 0 {manageable} {default}
|
||||||
|
bool G1PeriodicGCInvokesConcurrent = true {product} {default}
|
||||||
|
double G1PeriodicGCSystemLoadThreshold = 0.000000 {manageable} {default}
|
||||||
|
uint G1RSetUpdatingPauseTimePercent = 10 {product} {default}
|
||||||
|
uint G1RefProcDrainInterval = 1000 {product} {default}
|
||||||
|
uint G1ReservePercent = 10 {product} {default}
|
||||||
|
uint G1SATBBufferEnqueueingThresholdPercent = 60 {product} {default}
|
||||||
|
size_t G1SATBBufferSize = 1024 {product} {default}
|
||||||
|
size_t G1UpdateBufferSize = 256 {product} {default}
|
||||||
|
bool G1UseAdaptiveIHOP = true {product} {default}
|
||||||
|
uint GCCardSizeInBytes = 512 {product} {default}
|
||||||
|
uint GCDrainStackTargetSize = 64 {product} {default}
|
||||||
|
uint GCHeapFreeLimit = 2 {product} {default}
|
||||||
|
uintx GCPauseIntervalMillis = 0 {product} {default}
|
||||||
|
uint GCTimeLimit = 98 {product} {default}
|
||||||
|
uint GCTimeRatio = 99 {product} {default}
|
||||||
|
size_t HeapBaseMinAddress = 2147483648 {pd product} {default}
|
||||||
|
bool HeapDumpAfterFullGC = false {manageable} {default}
|
||||||
|
bool HeapDumpBeforeFullGC = false {manageable} {default}
|
||||||
|
int HeapDumpGzipLevel = 0 {manageable} {default}
|
||||||
|
bool HeapDumpOnOutOfMemoryError = false {manageable} {default}
|
||||||
|
ccstr HeapDumpPath = {manageable} {default}
|
||||||
|
uintx HeapMaximumCompactionInterval = 20 {product} {default}
|
||||||
|
uintx HeapSearchSteps = 3 {product} {default}
|
||||||
|
size_t HeapSizePerGCThread = 43620760 {product} {default}
|
||||||
|
bool IgnoreEmptyClassPaths = false {product} {default}
|
||||||
|
bool IgnoreUnrecognizedVMOptions = false {product} {default}
|
||||||
|
uintx IncreaseFirstTierCompileThresholdAt = 50 {product} {default}
|
||||||
|
bool IncrementalInline = true {C2 product} {default}
|
||||||
|
uintx InitialCodeCacheSize = 2555904 {pd product} {default}
|
||||||
|
size_t InitialHeapSize = 2147483648 {product} {command line}
|
||||||
|
double InitialRAMPercentage = 1.562500 {product} {default}
|
||||||
|
uintx InitialSurvivorRatio = 8 {product} {default}
|
||||||
|
uint InitialTenuringThreshold = 7 {product} {default}
|
||||||
|
uint InitiatingHeapOccupancyPercent = 45 {product} {default}
|
||||||
|
bool Inline = true {product} {default}
|
||||||
|
ccstr InlineDataFile = {product} {default}
|
||||||
|
intx InlineSmallCode = 2500 {C2 pd product} {default}
|
||||||
|
bool InlineSynchronizedMethods = true {C1 product} {default}
|
||||||
|
intx InteriorEntryAlignment = 16 {C2 pd product} {default}
|
||||||
|
intx InterpreterProfilePercentage = 33 {product} {default}
|
||||||
|
bool JavaMonitorsInStackTrace = true {product} {default}
|
||||||
|
int JavaPriority10_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority1_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority2_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority3_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority4_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority5_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority6_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority7_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority8_To_OSPriority = -1 {product} {default}
|
||||||
|
int JavaPriority9_To_OSPriority = -1 {product} {default}
|
||||||
|
size_t LargePageHeapSizeThreshold = 134217728 {product} {default}
|
||||||
|
size_t LargePageSizeInBytes = 0 {product} {default}
|
||||||
|
intx LiveNodeCountInliningCutoff = 40000 {C2 product} {default}
|
||||||
|
int LockingMode = 2 {product} {default}
|
||||||
|
ccstr LogClassLoadingCauseFor = {product} {default}
|
||||||
|
intx LoopMaxUnroll = 16 {C2 product} {default}
|
||||||
|
intx LoopOptsCount = 43 {C2 product} {default}
|
||||||
|
intx LoopPercentProfileLimit = 10 {C2 pd product} {default}
|
||||||
|
uintx LoopStripMiningIter = 1000 {C2 product} {default}
|
||||||
|
uintx LoopStripMiningIterShortLoop = 100 {C2 product} {default}
|
||||||
|
intx LoopUnrollLimit = 60 {C2 pd product} {default}
|
||||||
|
intx LoopUnrollMin = 4 {C2 product} {default}
|
||||||
|
bool LoopUnswitching = true {C2 product} {default}
|
||||||
|
bool ManagementServer = false {product} {default}
|
||||||
|
size_t MarkStackSize = 4194304 {product} {default}
|
||||||
|
size_t MarkStackSizeMax = 536870912 {product} {default}
|
||||||
|
uint MarkSweepAlwaysCompactCount = 4 {product} {default}
|
||||||
|
uint MarkSweepDeadRatio = 5 {product} {default}
|
||||||
|
intx MaxBCEAEstimateLevel = 5 {product} {default}
|
||||||
|
intx MaxBCEAEstimateSize = 150 {product} {default}
|
||||||
|
uint64_t MaxDirectMemorySize = 0 {product} {default}
|
||||||
|
bool MaxFDLimit = true {product} {default}
|
||||||
|
uintx MaxGCPauseMillis = 18446744073709551614 {product} {default}
|
||||||
|
uintx MaxHeapFreeRatio = 70 {manageable} {default}
|
||||||
|
size_t MaxHeapSize = 2147483648 {product} {command line}
|
||||||
|
intx MaxInlineLevel = 15 {C2 product} {default}
|
||||||
|
intx MaxInlineSize = 35 {C2 product} {default}
|
||||||
|
intx MaxJNILocalCapacity = 65536 {product} {default}
|
||||||
|
int MaxJavaStackTraceDepth = 1024 {product} {default}
|
||||||
|
intx MaxJumpTableSize = 65000 {C2 product} {default}
|
||||||
|
intx MaxJumpTableSparseness = 5 {C2 product} {default}
|
||||||
|
intx MaxLabelRootDepth = 1100 {C2 product} {default}
|
||||||
|
intx MaxLoopPad = 15 {C2 product} {default}
|
||||||
|
size_t MaxMetaspaceExpansion = 5439488 {product} {default}
|
||||||
|
uint MaxMetaspaceFreeRatio = 70 {product} {default}
|
||||||
|
size_t MaxMetaspaceSize = 18446744073709551615 {product} {default}
|
||||||
|
size_t MaxNewSize = 18446744073709551615 {product} {default}
|
||||||
|
intx MaxNodeLimit = 80000 {C2 product} {default}
|
||||||
|
uint64_t MaxRAM = 137438953472 {pd product} {default}
|
||||||
|
double MaxRAMPercentage = 25.000000 {product} {default}
|
||||||
|
intx MaxRecursiveInlineLevel = 1 {C2 product} {default}
|
||||||
|
uint MaxTenuringThreshold = 14 {product} {default}
|
||||||
|
intx MaxTrivialSize = 6 {C2 product} {default}
|
||||||
|
intx MaxVectorSize = 32 {C2 product} {default}
|
||||||
|
size_t MetaspaceSize = 22020096 {product} {default}
|
||||||
|
bool MethodFlushing = true {product} {default}
|
||||||
|
size_t MinHeapDeltaBytes = 2097152 {product} {ergonomic}
|
||||||
|
uintx MinHeapFreeRatio = 40 {manageable} {default}
|
||||||
|
size_t MinHeapSize = 2147483648 {product} {command line}
|
||||||
|
intx MinJumpTableSize = 10 {C2 pd product} {default}
|
||||||
|
size_t MinMetaspaceExpansion = 327680 {product} {default}
|
||||||
|
uint MinMetaspaceFreeRatio = 40 {product} {default}
|
||||||
|
double MinRAMPercentage = 50.000000 {product} {default}
|
||||||
|
uintx MinSurvivorRatio = 3 {product} {default}
|
||||||
|
size_t MinTLABSize = 2048 {product} {default}
|
||||||
|
intx MultiArrayExpandLimit = 6 {C2 product} {default}
|
||||||
|
uintx NUMAChunkResizeWeight = 20 {product} {default}
|
||||||
|
size_t NUMAInterleaveGranularity = 2097152 {product} {default}
|
||||||
|
size_t NUMASpaceResizeRate = 1073741824 {product} {default}
|
||||||
|
bool NUMAStats = false {product} {default}
|
||||||
|
ccstr NativeMemoryTracking = off {product} {default}
|
||||||
|
bool NeverActAsServerClassMachine = false {pd product} {default}
|
||||||
|
bool NeverTenure = false {product} {default}
|
||||||
|
uintx NewRatio = 2 {product} {default}
|
||||||
|
size_t NewSize = 1363144 {product} {default}
|
||||||
|
size_t NewSizeThreadIncrease = 5320 {pd product} {default}
|
||||||
|
intx NmethodSweepActivity = 4 {product} {default}
|
||||||
|
intx NodeLimitFudgeFactor = 2000 {C2 product} {default}
|
||||||
|
uintx NonNMethodCodeHeapSize = 5832704 {pd product} {ergonomic}
|
||||||
|
uintx NonProfiledCodeHeapSize = 122945536 {pd product} {ergonomic}
|
||||||
|
intx NumberOfLoopInstrToAlign = 4 {C2 product} {default}
|
||||||
|
int ObjectAlignmentInBytes = 8 {product lp64_product} {default}
|
||||||
|
size_t OldPLABSize = 1024 {product} {default}
|
||||||
|
bool OmitStackTraceInFastThrow = true {product} {default}
|
||||||
|
ccstrlist OnError = {product} {default}
|
||||||
|
ccstrlist OnOutOfMemoryError = {product} {default}
|
||||||
|
intx OnStackReplacePercentage = 140 {pd product} {default}
|
||||||
|
bool OptimizeFill = false {C2 product} {default}
|
||||||
|
bool OptimizePtrCompare = true {C2 product} {default}
|
||||||
|
bool OptimizeStringConcat = true {C2 product} {default}
|
||||||
|
bool OptoBundling = false {C2 pd product} {default}
|
||||||
|
intx OptoLoopAlignment = 16 {pd product} {default}
|
||||||
|
bool OptoRegScheduling = true {C2 pd product} {default}
|
||||||
|
bool OptoScheduling = false {C2 pd product} {default}
|
||||||
|
uint PLABWeight = 75 {product} {default}
|
||||||
|
bool PSChunkLargeArrays = true {product} {default}
|
||||||
|
int ParGCArrayScanChunk = 50 {product} {default}
|
||||||
|
uint ParallelGCBufferWastePct = 10 {product} {default}
|
||||||
|
uint ParallelGCThreads = 8 {product} {default}
|
||||||
|
bool ParallelRefProcBalancingEnabled = true {product} {default}
|
||||||
|
bool ParallelRefProcEnabled = false {product} {default}
|
||||||
|
bool PartialPeelAtUnsignedTests = true {C2 product} {default}
|
||||||
|
bool PartialPeelLoop = true {C2 product} {default}
|
||||||
|
intx PartialPeelNewPhiDelta = 0 {C2 product} {default}
|
||||||
|
uint PausePadding = 1 {product} {default}
|
||||||
|
intx PerBytecodeRecompilationCutoff = 200 {product} {default}
|
||||||
|
intx PerBytecodeTrapLimit = 4 {product} {default}
|
||||||
|
intx PerMethodRecompilationCutoff = 400 {product} {default}
|
||||||
|
intx PerMethodTrapLimit = 100 {product} {default}
|
||||||
|
bool PerfAllowAtExitRegistration = false {product} {default}
|
||||||
|
bool PerfBypassFileSystemCheck = false {product} {default}
|
||||||
|
int PerfDataMemorySize = 32768 {product} {default}
|
||||||
|
ccstr PerfDataSaveFile = {product} {default}
|
||||||
|
bool PerfDataSaveToFile = false {product} {default}
|
||||||
|
bool PerfDisableSharedMem = false {product} {default}
|
||||||
|
int PerfMaxStringConstLength = 1024 {product} {default}
|
||||||
|
size_t PreTouchParallelChunkSize = 1073741824 {pd product} {default}
|
||||||
|
bool PreferInterpreterNativeStubs = false {pd product} {default}
|
||||||
|
intx PrefetchCopyIntervalInBytes = 576 {product} {default}
|
||||||
|
intx PrefetchScanIntervalInBytes = 576 {product} {default}
|
||||||
|
bool PreserveFramePointer = false {pd product} {default}
|
||||||
|
size_t PretenureSizeThreshold = 0 {product} {default}
|
||||||
|
bool PrintClassHistogram = false {manageable} {default}
|
||||||
|
bool PrintCodeCache = false {product} {default}
|
||||||
|
bool PrintCodeCacheOnCompilation = false {product} {default}
|
||||||
|
bool PrintCommandLineFlags = false {product} {default}
|
||||||
|
bool PrintCompilation = false {product} {default}
|
||||||
|
bool PrintConcurrentLocks = false {manageable} {default}
|
||||||
|
bool PrintExtendedThreadInfo = false {product} {default}
|
||||||
|
bool PrintFlagsFinal = true {product} {command line}
|
||||||
|
bool PrintFlagsInitial = false {product} {default}
|
||||||
|
bool PrintFlagsRanges = false {product} {default}
|
||||||
|
bool PrintGC = false {product} {default}
|
||||||
|
bool PrintGCDetails = false {product} {default}
|
||||||
|
bool PrintHeapAtSIGBREAK = true {product} {default}
|
||||||
|
bool PrintSharedArchiveAndExit = false {product} {default}
|
||||||
|
bool PrintStringTableStatistics = false {product} {default}
|
||||||
|
bool PrintTieredEvents = false {product} {default}
|
||||||
|
bool PrintVMOptions = false {product} {default}
|
||||||
|
bool PrintWarnings = true {product} {default}
|
||||||
|
bool ProfileExceptionHandlers = true {product} {default}
|
||||||
|
bool ProfileInterpreter = true {pd product} {default}
|
||||||
|
intx ProfileMaturityPercentage = 20 {product} {default}
|
||||||
|
uintx ProfiledCodeHeapSize = 122880000 {pd product} {ergonomic}
|
||||||
|
uint PromotedPadding = 3 {product} {default}
|
||||||
|
uintx QueuedAllocationWarningCount = 0 {product} {default}
|
||||||
|
bool RangeCheckElimination = true {product} {default}
|
||||||
|
bool ReassociateInvariants = true {C2 product} {default}
|
||||||
|
bool RecordDynamicDumpInfo = false {product} {default}
|
||||||
|
bool ReduceBulkZeroing = true {C2 product} {default}
|
||||||
|
bool ReduceFieldZeroing = true {C2 product} {default}
|
||||||
|
bool ReduceInitialCardMarks = true {C2 product} {default}
|
||||||
|
bool ReduceSignalUsage = false {product} {default}
|
||||||
|
bool RelaxAccessControlCheck = false {product} {default}
|
||||||
|
ccstr ReplayDataFile = {product} {default}
|
||||||
|
uintx ReservedCodeCacheSize = 251658240 {pd product} {ergonomic}
|
||||||
|
bool ResizePLAB = true {product} {default}
|
||||||
|
bool ResizeTLAB = true {product} {default}
|
||||||
|
bool RestoreMXCSROnJNICalls = false {product} {default}
|
||||||
|
bool RestrictContended = true {product} {default}
|
||||||
|
bool RestrictReservedStack = true {product} {default}
|
||||||
|
bool RewriteBytecodes = true {pd product} {default}
|
||||||
|
bool RewriteFrequentPairs = true {pd product} {default}
|
||||||
|
bool SafepointTimeout = false {product} {default}
|
||||||
|
double SafepointTimeoutDelay = 10000.000000 {product} {default}
|
||||||
|
bool SegmentedCodeCache = true {product} {ergonomic}
|
||||||
|
double SelfDestructTimer = 0.000000 {product} {default}
|
||||||
|
ccstr SharedArchiveConfigFile = {product} {default}
|
||||||
|
ccstr SharedArchiveFile = {product} {default}
|
||||||
|
size_t SharedBaseAddress = 2600468480 {product} {default}
|
||||||
|
ccstr SharedClassListFile = {product} {default}
|
||||||
|
uint SharedSymbolTableBucketSize = 4 {product} {default}
|
||||||
|
bool ShowCodeDetailsInExceptionMessages = true {manageable} {default}
|
||||||
|
bool ShowMessageBoxOnError = false {product} {default}
|
||||||
|
bool ShrinkHeapInSteps = true {product} {default}
|
||||||
|
size_t SoftMaxHeapSize = 2147483648 {manageable} {ergonomic}
|
||||||
|
intx SoftRefLRUPolicyMSPerMB = 1000 {product} {default}
|
||||||
|
bool SplitIfBlocks = true {C2 product} {default}
|
||||||
|
intx StackRedPages = 1 {pd product} {default}
|
||||||
|
intx StackReservedPages = 0 {pd product} {default}
|
||||||
|
intx StackShadowPages = 8 {pd product} {default}
|
||||||
|
bool StackTraceInThrowable = true {product} {default}
|
||||||
|
intx StackYellowPages = 3 {pd product} {default}
|
||||||
|
uintx StartAggressiveSweepingAt = 10 {product} {default}
|
||||||
|
bool StartAttachListener = false {product} {default}
|
||||||
|
ccstr StartFlightRecording = {product} {default}
|
||||||
|
uint StringDeduplicationAgeThreshold = 3 {product} {default}
|
||||||
|
uintx StringTableSize = 65536 {product} {default}
|
||||||
|
bool SuperWordLoopUnrollAnalysis = true {C2 pd product} {default}
|
||||||
|
bool SuperWordReductions = true {C2 product} {default}
|
||||||
|
bool SuppressFatalErrorMessage = false {product} {default}
|
||||||
|
uint SurvivorPadding = 3 {product} {default}
|
||||||
|
uintx SurvivorRatio = 8 {product} {default}
|
||||||
|
double SweeperThreshold = 15.000000 {product} {default}
|
||||||
|
uintx TLABAllocationWeight = 35 {product} {default}
|
||||||
|
uintx TLABRefillWasteFraction = 64 {product} {default}
|
||||||
|
size_t TLABSize = 262144 {product} {default}
|
||||||
|
uintx TLABWasteIncrement = 4 {product} {default}
|
||||||
|
uintx TLABWasteTargetPercent = 1 {product} {default}
|
||||||
|
uint TargetPLABWastePct = 10 {product} {default}
|
||||||
|
uint TargetSurvivorRatio = 50 {product} {default}
|
||||||
|
uint TenuredGenerationSizeIncrement = 20 {product} {default}
|
||||||
|
uint TenuredGenerationSizeSupplement = 80 {product} {default}
|
||||||
|
uintx TenuredGenerationSizeSupplementDecay = 2 {product} {default}
|
||||||
|
int ThreadPriorityPolicy = 0 {product} {default}
|
||||||
|
bool ThreadPriorityVerbose = false {product} {default}
|
||||||
|
intx ThreadStackSize = 0 {pd product} {default}
|
||||||
|
uint ThresholdTolerance = 10 {product} {default}
|
||||||
|
intx Tier0BackedgeNotifyFreqLog = 10 {product} {default}
|
||||||
|
intx Tier0InvokeNotifyFreqLog = 7 {product} {default}
|
||||||
|
intx Tier0ProfilingStartPercentage = 200 {product} {default}
|
||||||
|
intx Tier23InlineeNotifyFreqLog = 20 {product} {default}
|
||||||
|
intx Tier2BackEdgeThreshold = 0 {product} {default}
|
||||||
|
intx Tier2BackedgeNotifyFreqLog = 14 {product} {default}
|
||||||
|
intx Tier2CompileThreshold = 0 {product} {default}
|
||||||
|
intx Tier2InvokeNotifyFreqLog = 11 {product} {default}
|
||||||
|
intx Tier3BackEdgeThreshold = 60000 {product} {default}
|
||||||
|
intx Tier3BackedgeNotifyFreqLog = 13 {product} {default}
|
||||||
|
intx Tier3CompileThreshold = 2000 {product} {default}
|
||||||
|
intx Tier3DelayOff = 2 {product} {default}
|
||||||
|
intx Tier3DelayOn = 5 {product} {default}
|
||||||
|
intx Tier3InvocationThreshold = 200 {product} {default}
|
||||||
|
intx Tier3InvokeNotifyFreqLog = 10 {product} {default}
|
||||||
|
intx Tier3LoadFeedback = 5 {product} {default}
|
||||||
|
intx Tier3MinInvocationThreshold = 100 {product} {default}
|
||||||
|
intx Tier4BackEdgeThreshold = 40000 {product} {default}
|
||||||
|
intx Tier4CompileThreshold = 15000 {product} {default}
|
||||||
|
intx Tier4InvocationThreshold = 5000 {product} {default}
|
||||||
|
intx Tier4LoadFeedback = 3 {product} {default}
|
||||||
|
intx Tier4MinInvocationThreshold = 600 {product} {default}
|
||||||
|
bool TieredCompilation = true {pd product} {default}
|
||||||
|
intx TieredCompileTaskTimeout = 50 {product} {default}
|
||||||
|
intx TieredRateUpdateMaxTime = 25 {product} {default}
|
||||||
|
intx TieredRateUpdateMinTime = 1 {product} {default}
|
||||||
|
intx TieredStopAtLevel = 4 {product} {default}
|
||||||
|
ccstr TraceJVMTI = {product} {default}
|
||||||
|
intx TrackedInitializationLimit = 50 {C2 product} {default}
|
||||||
|
bool TrapBasedNullChecks = false {pd product} {default}
|
||||||
|
bool TrapBasedRangeChecks = false {C2 pd product} {default}
|
||||||
|
uint TrimNativeHeapInterval = 0 {product} {default}
|
||||||
|
int TypeProfileArgsLimit = 2 {product} {default}
|
||||||
|
uint TypeProfileLevel = 111 {pd product} {default}
|
||||||
|
intx TypeProfileMajorReceiverPercent = 90 {C2 product} {default}
|
||||||
|
int TypeProfileParmsLimit = 2 {product} {default}
|
||||||
|
intx TypeProfileSubTypeCheckCommonThreshold = 50 {C2 product} {default}
|
||||||
|
intx TypeProfileWidth = 2 {product} {default}
|
||||||
|
int UnguardOnExecutionViolation = 0 {product} {default}
|
||||||
|
bool UseAES = true {product} {default}
|
||||||
|
int UseAVX = 2 {ARCH product} {default}
|
||||||
|
bool UseAdaptiveGenerationSizePolicyAtMajorCollection = true {product} {default}
|
||||||
|
bool UseAdaptiveGenerationSizePolicyAtMinorCollection = true {product} {default}
|
||||||
|
bool UseAdaptiveNUMAChunkSizing = true {product} {default}
|
||||||
|
bool UseAdaptiveSizeDecayMajorGCCost = true {product} {default}
|
||||||
|
bool UseAdaptiveSizePolicy = true {product} {default}
|
||||||
|
bool UseAdaptiveSizePolicyFootprintGoal = true {product} {default}
|
||||||
|
bool UseAdaptiveSizePolicyWithSystemGC = false {product} {default}
|
||||||
|
bool UseAddressNop = true {ARCH product} {default}
|
||||||
|
bool UseAllWindowsProcessorGroups = false {product} {default}
|
||||||
|
bool UseBASE64Intrinsics = true {product} {default}
|
||||||
|
bool UseBMI1Instructions = true {ARCH product} {default}
|
||||||
|
bool UseBMI2Instructions = true {ARCH product} {default}
|
||||||
|
bool UseBimorphicInlining = true {C2 product} {default}
|
||||||
|
bool UseCLMUL = true {ARCH product} {default}
|
||||||
|
bool UseCMoveUnconditionally = false {C2 product} {default}
|
||||||
|
bool UseCodeCacheFlushing = true {product} {default}
|
||||||
|
bool UseCompactObjectHeaders = false {product lp64_product} {default}
|
||||||
|
bool UseCompiler = true {product} {default}
|
||||||
|
bool UseCompressedClassPointers = true {product lp64_product} {default}
|
||||||
|
bool UseCompressedOops = false {product lp64_product} {ergonomic}
|
||||||
|
bool UseCondCardMark = false {product} {default}
|
||||||
|
bool UseCountLeadingZerosInstruction = true {ARCH product} {default}
|
||||||
|
bool UseCountTrailingZerosInstruction = true {ARCH product} {default}
|
||||||
|
bool UseCountedLoopSafepoints = true {C2 product} {default}
|
||||||
|
bool UseDivMod = true {C2 product} {default}
|
||||||
|
bool UseDynamicNumberOfCompilerThreads = true {product} {default}
|
||||||
|
bool UseDynamicNumberOfGCThreads = true {product} {default}
|
||||||
|
bool UseFMA = true {product} {default}
|
||||||
|
bool UseFPUForSpilling = true {C2 product} {default}
|
||||||
|
bool UseFastJNIAccessors = true {product} {default}
|
||||||
|
bool UseFastStosb = false {ARCH product} {default}
|
||||||
|
bool UseG1GC = false {product} {default}
|
||||||
|
bool UseGCOverheadLimit = true {product} {default}
|
||||||
|
bool UseInlineCaches = true {product} {default}
|
||||||
|
bool UseInterpreter = true {product} {default}
|
||||||
|
bool UseJumpTables = true {C2 product} {default}
|
||||||
|
bool UseLargePages = false {pd product} {default}
|
||||||
|
bool UseLargePagesIndividualAllocation = false {pd product} {ergonomic}
|
||||||
|
bool UseLoopCounter = true {product} {default}
|
||||||
|
bool UseLoopInvariantCodeMotion = true {C1 product} {default}
|
||||||
|
bool UseLoopPredicate = true {C2 product} {default}
|
||||||
|
bool UseMaximumCompactionOnSystemGC = true {product} {default}
|
||||||
|
bool UseNUMA = false {product} {default}
|
||||||
|
bool UseNUMAInterleaving = false {product} {default}
|
||||||
|
bool UseNewLongLShift = true {ARCH product} {default}
|
||||||
|
bool UseOSErrorReporting = false {product} {default}
|
||||||
|
bool UseOnStackReplacement = true {pd product} {default}
|
||||||
|
bool UseOnlyInlinedBimorphic = true {C2 product} {default}
|
||||||
|
bool UsePSAdaptiveSurvivorSizePolicy = true {product} {default}
|
||||||
|
bool UseParallelGC = false {product} {default}
|
||||||
|
bool UsePerfData = true {product} {default}
|
||||||
|
bool UsePopCountInstruction = true {product} {default}
|
||||||
|
bool UseProfiledLoopPredicate = true {C2 product} {default}
|
||||||
|
bool UseSHA = true {product} {default}
|
||||||
|
int UseSSE = 4 {ARCH product} {default}
|
||||||
|
bool UseSSE42Intrinsics = true {ARCH product} {default}
|
||||||
|
bool UseSerialGC = false {product} {default}
|
||||||
|
bool UseShenandoahGC = false {product} {default}
|
||||||
|
bool UseSignalChaining = true {product} {default}
|
||||||
|
bool UseStoreImmI16 = true {ARCH product} {default}
|
||||||
|
bool UseStringDeduplication = false {product} {default}
|
||||||
|
bool UseSubwordForMaxVector = true {C2 product} {default}
|
||||||
|
bool UseSuperWord = true {C2 product} {default}
|
||||||
|
bool UseSystemMemoryBarrier = false {product} {default}
|
||||||
|
bool UseTLAB = true {product} {default}
|
||||||
|
bool UseThreadPriorities = true {pd product} {default}
|
||||||
|
bool UseTypeProfile = true {product} {default}
|
||||||
|
bool UseTypeSpeculation = true {C2 product} {default}
|
||||||
|
bool UseUnalignedLoadStores = true {ARCH product} {default}
|
||||||
|
bool UseVectorCmov = false {C2 product} {default}
|
||||||
|
bool UseXMMForArrayCopy = true {product} {default}
|
||||||
|
bool UseXMMForObjInit = true {ARCH product} {default}
|
||||||
|
bool UseXmmI2D = true {ARCH product} {default}
|
||||||
|
bool UseXmmI2F = true {ARCH product} {default}
|
||||||
|
bool UseXmmLoadAndClearUpper = true {ARCH product} {default}
|
||||||
|
bool UseXmmRegToRegMoveAll = true {ARCH product} {default}
|
||||||
|
bool UseZGC = true {product} {command line}
|
||||||
|
intx UserThreadWaitAttemptsAtExit = 30 {product} {default}
|
||||||
|
int VMThreadPriority = -1 {product} {default}
|
||||||
|
intx VMThreadStackSize = 0 {pd product} {default}
|
||||||
|
intx ValueMapInitialSize = 11 {C1 product} {default}
|
||||||
|
intx ValueMapMaxLoopSize = 8 {C1 product} {default}
|
||||||
|
intx ValueSearchLimit = 1000 {C2 product} {default}
|
||||||
|
bool VerifySharedSpaces = false {product} {default}
|
||||||
|
uint YoungGenerationSizeIncrement = 20 {product} {default}
|
||||||
|
uint YoungGenerationSizeSupplement = 80 {product} {default}
|
||||||
|
uintx YoungGenerationSizeSupplementDecay = 8 {product} {default}
|
||||||
|
size_t YoungPLABSize = 4096 {product} {default}
|
||||||
|
double ZAllocationSpikeTolerance = 2.000000 {product} {default}
|
||||||
|
double ZCollectionInterval = 0.000000 {product} {default}
|
||||||
|
double ZCollectionIntervalMajor = -1.000000 {product} {default}
|
||||||
|
double ZCollectionIntervalMinor = -1.000000 {product} {default}
|
||||||
|
bool ZCollectionIntervalOnly = false {product} {default}
|
||||||
|
double ZFragmentationLimit = 5.000000 {product} {default}
|
||||||
|
bool ZProactive = true {product} {default}
|
||||||
|
bool ZUncommit = false {product} {ergonomic}
|
||||||
|
uintx ZUncommitDelay = 300 {product} {default}
|
||||||
|
double ZYoungCompactionLimit = 25.000000 {product} {default}
|
||||||
|
bool ZeroTLAB = false {product} {default}
|
||||||
|
java version "25.0.3" 2026-04-21 LTS
|
||||||
|
Java(TM) SE Runtime Environment (build 25.0.3+9-LTS-195)
|
||||||
|
Java HotSpot(TM) 64-Bit Server VM (build 25.0.3+9-LTS-195, mixed mode, sharing)
|
||||||
47
results/03-latency-g1.txt
Normal file
47
results/03-latency-g1.txt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
==============================================================================
|
||||||
|
ZGC / G1 open-loop latency harness
|
||||||
|
==============================================================================
|
||||||
|
collector : G1 Young Generation + G1 Concurrent GC + G1 Old Generation
|
||||||
|
java : 25.0.3+9-LTS-195
|
||||||
|
threads=4 rate=20,000/s warmup=20s measure=60s
|
||||||
|
liveSet=150,000 entries x 4,096 B (~585 MB live)
|
||||||
|
garbage=16,384 B/request reads=64/request mutationProbability=0.20
|
||||||
|
nominal allocation rate ~328 MB/s seed=20260730
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
building live set ... 150,000 entries, ~589 MB live, took 266 ms
|
||||||
|
warming up for 20 s at 20,000 req/s ...
|
||||||
|
measuring for 60 s at 20,000 req/s ...
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
achieved throughput : 19,976 req/s over 60.1 s
|
||||||
|
|
||||||
|
RESPONSE TIME (from intended arrival -- the honest number) (n=1,200,000)
|
||||||
|
mean 0.388 ms
|
||||||
|
p50 0.005 ms
|
||||||
|
p90 0.009 ms
|
||||||
|
p99 8.042 ms
|
||||||
|
p99.9 95.169 ms
|
||||||
|
p99.99 126.949 ms
|
||||||
|
max 133.018 ms
|
||||||
|
|
||||||
|
SERVICE TIME (from actual start -- hides queueing) (n=1,200,000)
|
||||||
|
mean 0.007 ms
|
||||||
|
p50 0.005 ms
|
||||||
|
p90 0.008 ms
|
||||||
|
p99 0.016 ms
|
||||||
|
p99.9 0.033 ms
|
||||||
|
p99.99 0.171 ms
|
||||||
|
max 113.864 ms
|
||||||
|
|
||||||
|
coordinated-omission gap at p99.9 : 95.169 ms response vs 0.033 ms service (2910.4x)
|
||||||
|
|
||||||
|
GC activity during the measured window
|
||||||
|
G1 Young Generation collections=36 time= 604 ms [stop-the-world]
|
||||||
|
G1 Concurrent GC collections=19 time= 15 ms [cycle (mostly concurrent)]
|
||||||
|
G1 Old Generation collections=2 time= 208 ms [stop-the-world]
|
||||||
|
|
||||||
|
CSV (response time)
|
||||||
|
collector,n,mean_ms,p50_ms,p90_ms,p99_ms,p999_ms,p9999_ms,max_ms
|
||||||
|
G1 Young Generation + G1 Concurrent GC + G1 Old Generation,1200000,0.388,0.005,0.009,8.042,95.169,126.949,133.018
|
||||||
|
|
||||||
|
liveSet checksum: 0
|
||||||
48
results/03-latency-zgc.txt
Normal file
48
results/03-latency-zgc.txt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
==============================================================================
|
||||||
|
ZGC / G1 open-loop latency harness
|
||||||
|
==============================================================================
|
||||||
|
collector : ZGC Minor Cycles + ZGC Minor Pauses + ZGC Major Cycles + ZGC Major Pauses
|
||||||
|
java : 25.0.3+9-LTS-195
|
||||||
|
threads=4 rate=20,000/s warmup=20s measure=60s
|
||||||
|
liveSet=150,000 entries x 4,096 B (~585 MB live)
|
||||||
|
garbage=16,384 B/request reads=64/request mutationProbability=0.20
|
||||||
|
nominal allocation rate ~328 MB/s seed=20260730
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
building live set ... 150,000 entries, ~589 MB live, took 310 ms
|
||||||
|
warming up for 20 s at 20,000 req/s ...
|
||||||
|
measuring for 60 s at 20,000 req/s ...
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
achieved throughput : 19,973 req/s over 60.1 s
|
||||||
|
|
||||||
|
RESPONSE TIME (from intended arrival -- the honest number) (n=1,200,000)
|
||||||
|
mean 0.016 ms
|
||||||
|
p50 0.006 ms
|
||||||
|
p90 0.009 ms
|
||||||
|
p99 0.029 ms
|
||||||
|
p99.9 1.437 ms
|
||||||
|
p99.99 23.675 ms
|
||||||
|
max 32.178 ms
|
||||||
|
|
||||||
|
SERVICE TIME (from actual start -- hides queueing) (n=1,200,000)
|
||||||
|
mean 0.007 ms
|
||||||
|
p50 0.006 ms
|
||||||
|
p90 0.009 ms
|
||||||
|
p99 0.025 ms
|
||||||
|
p99.9 0.143 ms
|
||||||
|
p99.99 0.269 ms
|
||||||
|
max 8.353 ms
|
||||||
|
|
||||||
|
coordinated-omission gap at p99.9 : 1.437 ms response vs 0.143 ms service (10.0x)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
CSV (response time)
|
||||||
|
collector,n,mean_ms,p50_ms,p90_ms,p99_ms,p999_ms,p9999_ms,max_ms
|
||||||
|
ZGC Minor Cycles + ZGC Minor Pauses + ZGC Major Cycles + ZGC Major Pauses,1200000,0.016,0.006,0.009,0.029,1.437,23.675,32.178
|
||||||
|
|
||||||
|
liveSet checksum: 1
|
||||||
20
results/04-stalls-g1.txt
Normal file
20
results/04-stalls-g1.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
max heap : 512 MB
|
||||||
|
target live set : 281 MB (55% of heap)
|
||||||
|
threads : 12, duration: 20 s
|
||||||
|
|
||||||
|
filling live set ...
|
||||||
|
live set filled: 4,505 chunks x 64 KB
|
||||||
|
now allocating hard for 20 s -- expect stalls
|
||||||
|
|
||||||
|
allocation phase done
|
||||||
|
|
||||||
|
========================================================================
|
||||||
|
JFR summary (C:\Users\Ankur\allocation-stalls.jfr)
|
||||||
|
========================================================================
|
||||||
|
young collections : 0
|
||||||
|
old collections : 0
|
||||||
|
page allocations : 0
|
||||||
|
ALLOCATION STALLS : 0
|
||||||
|
|
||||||
|
No stalls recorded. Either the heap was big enough, or you are not
|
||||||
|
running ZGC. Shrink -Xmx or raise -Dseconds and try again.
|
||||||
41
results/04-stalls-zgc-baseline.txt
Normal file
41
results/04-stalls-zgc-baseline.txt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
max heap : 512 MB
|
||||||
|
target live set : 281 MB (55% of heap)
|
||||||
|
threads : 12, duration: 20 s
|
||||||
|
|
||||||
|
filling live set ...
|
||||||
|
live set filled: 4,505 chunks x 64 KB
|
||||||
|
now allocating hard for 20 s -- expect stalls
|
||||||
|
|
||||||
|
allocation phase done
|
||||||
|
|
||||||
|
========================================================================
|
||||||
|
JFR summary (C:\Users\Ankur\allocation-stalls.jfr)
|
||||||
|
========================================================================
|
||||||
|
young collections : 1,771
|
||||||
|
old collections : 217
|
||||||
|
page allocations : 39,796
|
||||||
|
ALLOCATION STALLS : 32,200
|
||||||
|
|
||||||
|
total time threads spent stalled : 139,439 ms
|
||||||
|
mean stall : 4.33 ms
|
||||||
|
longest stall : 48.32 ms
|
||||||
|
|
||||||
|
stalls by ZGC page type
|
||||||
|
Small count=32,200 total=139,439 ms mean=4.33 ms
|
||||||
|
|
||||||
|
ten longest stalls
|
||||||
|
duration page size thread
|
||||||
|
48.32 ms Small 2097152 allocator-3
|
||||||
|
47.99 ms Small 2097152 allocator-10
|
||||||
|
46.39 ms Small 2097152 allocator-1
|
||||||
|
46.23 ms Small 2097152 allocator-0
|
||||||
|
46.19 ms Small 2097152 allocator-7
|
||||||
|
46.18 ms Small 2097152 allocator-5
|
||||||
|
45.11 ms Small 2097152 allocator-6
|
||||||
|
44.85 ms Small 2097152 allocator-7
|
||||||
|
44.68 ms Small 2097152 allocator-10
|
||||||
|
44.49 ms Small 2097152 allocator-0
|
||||||
|
|
||||||
|
Compare against your pause-time chart: none of the above is a pause.
|
||||||
|
Inspect the raw events, including the stack that was stalled, with:
|
||||||
|
jfr print --events ZAllocationStall allocation-stalls.jfr
|
||||||
41
results/04-stalls-zgc-softmax.txt
Normal file
41
results/04-stalls-zgc-softmax.txt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
max heap : 512 MB
|
||||||
|
target live set : 281 MB (55% of heap)
|
||||||
|
threads : 12, duration: 20 s
|
||||||
|
|
||||||
|
filling live set ...
|
||||||
|
live set filled: 4,505 chunks x 64 KB
|
||||||
|
now allocating hard for 20 s -- expect stalls
|
||||||
|
|
||||||
|
allocation phase done
|
||||||
|
|
||||||
|
========================================================================
|
||||||
|
JFR summary (C:\Users\Ankur\allocation-stalls.jfr)
|
||||||
|
========================================================================
|
||||||
|
young collections : 1,778
|
||||||
|
old collections : 217
|
||||||
|
page allocations : 39,685
|
||||||
|
ALLOCATION STALLS : 31,967
|
||||||
|
|
||||||
|
total time threads spent stalled : 138,521 ms
|
||||||
|
mean stall : 4.33 ms
|
||||||
|
longest stall : 51.41 ms
|
||||||
|
|
||||||
|
stalls by ZGC page type
|
||||||
|
Small count=31,967 total=138,521 ms mean=4.33 ms
|
||||||
|
|
||||||
|
ten longest stalls
|
||||||
|
duration page size thread
|
||||||
|
51.41 ms Small 2097152 allocator-10
|
||||||
|
50.18 ms Small 2097152 allocator-0
|
||||||
|
48.12 ms Small 2097152 allocator-5
|
||||||
|
47.93 ms Small 2097152 allocator-1
|
||||||
|
46.90 ms Small 2097152 allocator-1
|
||||||
|
46.14 ms Small 2097152 allocator-11
|
||||||
|
46.14 ms Small 2097152 allocator-11
|
||||||
|
46.08 ms Small 2097152 allocator-6
|
||||||
|
46.02 ms Small 2097152 allocator-4
|
||||||
|
45.68 ms Small 2097152 allocator-9
|
||||||
|
|
||||||
|
Compare against your pause-time chart: none of the above is a pause.
|
||||||
|
Inspect the raw events, including the stack that was stalled, with:
|
||||||
|
jfr print --events ZAllocationStall allocation-stalls.jfr
|
||||||
41
results/04-stalls-zgc-spiketolerance.txt
Normal file
41
results/04-stalls-zgc-spiketolerance.txt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
max heap : 512 MB
|
||||||
|
target live set : 281 MB (55% of heap)
|
||||||
|
threads : 12, duration: 20 s
|
||||||
|
|
||||||
|
filling live set ...
|
||||||
|
live set filled: 4,505 chunks x 64 KB
|
||||||
|
now allocating hard for 20 s -- expect stalls
|
||||||
|
|
||||||
|
allocation phase done
|
||||||
|
|
||||||
|
========================================================================
|
||||||
|
JFR summary (C:\Users\Ankur\allocation-stalls.jfr)
|
||||||
|
========================================================================
|
||||||
|
young collections : 1,722
|
||||||
|
old collections : 219
|
||||||
|
page allocations : 39,803
|
||||||
|
ALLOCATION STALLS : 32,442
|
||||||
|
|
||||||
|
total time threads spent stalled : 140,558 ms
|
||||||
|
mean stall : 4.33 ms
|
||||||
|
longest stall : 50.33 ms
|
||||||
|
|
||||||
|
stalls by ZGC page type
|
||||||
|
Small count=32,442 total=140,558 ms mean=4.33 ms
|
||||||
|
|
||||||
|
ten longest stalls
|
||||||
|
duration page size thread
|
||||||
|
50.33 ms Small 2097152 allocator-10
|
||||||
|
48.98 ms Small 2097152 allocator-5
|
||||||
|
48.96 ms Small 2097152 allocator-6
|
||||||
|
48.84 ms Small 2097152 allocator-7
|
||||||
|
48.79 ms Small 2097152 allocator-1
|
||||||
|
47.89 ms Small 2097152 allocator-0
|
||||||
|
47.33 ms Small 2097152 allocator-10
|
||||||
|
47.12 ms Small 2097152 allocator-3
|
||||||
|
46.96 ms Small 2097152 allocator-5
|
||||||
|
46.71 ms Small 2097152 allocator-1
|
||||||
|
|
||||||
|
Compare against your pause-time chart: none of the above is a pause.
|
||||||
|
Inspect the raw events, including the stack that was stalled, with:
|
||||||
|
jfr print --events ZAllocationStall allocation-stalls.jfr
|
||||||
19
results/05-pages-g1.txt
Normal file
19
results/05-pages-g1.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
==========================================================================
|
||||||
|
Page / region geometry as reported by THIS JVM
|
||||||
|
==========================================================================
|
||||||
|
max heap : 2,048 MB
|
||||||
|
UseG1GC / UseZGC : true / false
|
||||||
|
G1HeapRegionSize : 1,048,576 bytes (1 MB)
|
||||||
|
=> G1 humongous threshold: objects larger than 524,288 bytes (512 KB) go straight to old gen
|
||||||
|
|
||||||
|
No jdk.ZPageAllocation events (expected under G1).
|
||||||
|
|
||||||
|
Objects allocated in this run
|
||||||
|
65,536 bytes (64 KB) ordinary G1 allocation in an eden region
|
||||||
|
262,144 bytes (256 KB) ordinary G1 allocation in an eden region
|
||||||
|
524,288 bytes (512 KB) ordinary G1 allocation in an eden region
|
||||||
|
2,097,152 bytes (2 MB) HUMONGOUS on G1 -- allocated directly in old gen, spans whole regions
|
||||||
|
4,194,304 bytes (4 MB) HUMONGOUS on G1 -- allocated directly in old gen, spans whole regions
|
||||||
|
16,777,216 bytes (16 MB) HUMONGOUS on G1 -- allocated directly in old gen, spans whole regions
|
||||||
|
|
||||||
|
checksum: 6
|
||||||
33
results/05-pages-zgc.txt
Normal file
33
results/05-pages-zgc.txt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
==========================================================================
|
||||||
|
Page / region geometry as reported by THIS JVM
|
||||||
|
==========================================================================
|
||||||
|
max heap : 2,048 MB
|
||||||
|
UseG1GC / UseZGC : false / true
|
||||||
|
G1HeapRegionSize : 0 (ergonomic / not applicable)
|
||||||
|
=> ZGC small pages are 2 MB; objects up to 1/8 of a page (256 KB) use them.
|
||||||
|
Larger objects use a medium page, and anything bigger gets a Large page
|
||||||
|
of its own -- which ZGC frees rather than relocates.
|
||||||
|
|
||||||
|
ZGC internals flags visible in this JVM
|
||||||
|
ZFragmentationLimit = 5.0
|
||||||
|
ZCollectionIntervalMinor = -1.0
|
||||||
|
ZCollectionIntervalMajor = -1.0
|
||||||
|
ZAllocationSpikeTolerance = 2.0
|
||||||
|
ZUncommit = false
|
||||||
|
ZUncommitDelay = 300
|
||||||
|
SoftMaxHeapSize = 2,147,483,648 bytes (2048 MB)
|
||||||
|
|
||||||
|
ZGC pages actually allocated while creating the objects above
|
||||||
|
page type count total bytes
|
||||||
|
Large 2 25,165,824
|
||||||
|
Medium 1 33,554,432
|
||||||
|
|
||||||
|
Objects allocated in this run
|
||||||
|
65,536 bytes (64 KB) expect ZGC Small page (2 MB, shared with other objects)
|
||||||
|
262,144 bytes (256 KB) expect ZGC Medium page (shared; size is a range in JDK 25)
|
||||||
|
524,288 bytes (512 KB) expect ZGC Medium page (shared; size is a range in JDK 25)
|
||||||
|
2,097,152 bytes (2 MB) expect ZGC Medium page (shared; size is a range in JDK 25)
|
||||||
|
4,194,304 bytes (4 MB) expect ZGC Large page -- one page for this object, freed not relocated
|
||||||
|
16,777,216 bytes (16 MB) expect ZGC Large page -- one page for this object, freed not relocated
|
||||||
|
|
||||||
|
checksum: 6
|
||||||
40
results/06-gclog-g1.txt
Normal file
40
results/06-gclog-g1.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
==============================================================================
|
||||||
|
GC log analysis: g1-latency.log
|
||||||
|
==============================================================================
|
||||||
|
lines parsed : 1,257
|
||||||
|
young / minor cycles : 94
|
||||||
|
old / major cycles : 0
|
||||||
|
ZGC allocation stalls : 0
|
||||||
|
G1 evacuation failures : 16
|
||||||
|
full GCs : 4
|
||||||
|
|
||||||
|
STOP-THE-WORLD PAUSES (this is application freeze time)
|
||||||
|
count : 68
|
||||||
|
total : 1,139.624 ms
|
||||||
|
mean : 16.759 ms
|
||||||
|
p50 : 12.857 ms
|
||||||
|
p99 : 104.449 ms
|
||||||
|
max : 104.449 ms
|
||||||
|
|
||||||
|
by phase
|
||||||
|
Pause Young (Normal) n=17 total= 403.699 ms max=103.308 ms
|
||||||
|
Pause Young (Concurrent Start) n=11 total= 153.384 ms max= 20.587 ms
|
||||||
|
Pause Remark n=10 total= 13.736 ms max= 1.745 ms
|
||||||
|
Pause Cleanup n=9 total= 2.472 ms max= 0.438 ms
|
||||||
|
Pause Young (Prepare Mixed) n=8 total= 110.491 ms max= 24.639 ms
|
||||||
|
Pause Young (Mixed) n=11 total= 247.284 ms max= 30.879 ms
|
||||||
|
Pause Full n=2 total= 208.558 ms max=104.449 ms
|
||||||
|
|
||||||
|
CONCURRENT PHASES (application keeps running -- NOT freeze time)
|
||||||
|
Concurrent Scan Root Regions n=11 total= 12.317 ms max= 1.867 ms
|
||||||
|
Concurrent Mark From Roots n=11 total= 191.931 ms max=102.516 ms
|
||||||
|
Concurrent Preclean n=10 total= 0.265 ms max= 0.050 ms
|
||||||
|
Concurrent Mark n=10 total= 106.217 ms max= 14.524 ms
|
||||||
|
Concurrent Rebuild Remembered Sets and Scrub Regions n=10 total= 146.586 ms max=113.784 ms
|
||||||
|
Concurrent Clear Claimed Marks n=9 total= 0.227 ms max= 0.039 ms
|
||||||
|
Concurrent Cleanup for Next Mark n=9 total= 31.227 ms max= 4.862 ms
|
||||||
|
Concurrent Mark Cycle n=11 total= 406.062 ms max=125.183 ms
|
||||||
|
|
||||||
|
Adding the block above to the pause block would be a category error:
|
||||||
|
concurrent time overlaps application execution by design.
|
||||||
|
|
||||||
36
results/06-gclog-zgc.txt
Normal file
36
results/06-gclog-zgc.txt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
==============================================================================
|
||||||
|
GC log analysis: zgc-latency.log
|
||||||
|
==============================================================================
|
||||||
|
lines parsed : 2,247
|
||||||
|
young / minor cycles : 45
|
||||||
|
old / major cycles : 17
|
||||||
|
ZGC allocation stalls : 40
|
||||||
|
G1 evacuation failures : 0
|
||||||
|
full GCs : 0
|
||||||
|
|
||||||
|
STOP-THE-WORLD PAUSES (this is application freeze time)
|
||||||
|
count : 106
|
||||||
|
total : 1.503 ms
|
||||||
|
mean : 0.014 ms
|
||||||
|
p50 : 0.014 ms
|
||||||
|
p99 : 0.044 ms
|
||||||
|
max : 0.054 ms
|
||||||
|
|
||||||
|
by phase
|
||||||
|
Pause Mark Start (Major) n=8 total= 0.139 ms max= 0.021 ms
|
||||||
|
Pause Mark End n=38 total= 0.683 ms max= 0.044 ms
|
||||||
|
Pause Relocate Start n=38 total= 0.330 ms max= 0.021 ms
|
||||||
|
Pause Mark Start n=22 total= 0.351 ms max= 0.054 ms
|
||||||
|
|
||||||
|
CONCURRENT PHASES (application keeps running -- NOT freeze time)
|
||||||
|
Concurrent Mark n=38 total= 894.499 ms max= 78.403 ms
|
||||||
|
Concurrent Mark Free n=38 total= 0.097 ms max= 0.033 ms
|
||||||
|
Concurrent Reset Relocation Set n=38 total= 4.331 ms max= 0.934 ms
|
||||||
|
Concurrent Select Relocation Set n=38 total= 48.991 ms max= 5.372 ms
|
||||||
|
Concurrent Relocate n=38 total=1092.392 ms max=151.380 ms
|
||||||
|
Concurrent Process Non n=8 total= 5.635 ms max= 0.984 ms
|
||||||
|
Concurrent Remap Roots n=8 total= 10.919 ms max= 2.207 ms
|
||||||
|
|
||||||
|
Adding the block above to the pause block would be a category error:
|
||||||
|
concurrent time overlaps application execution by design.
|
||||||
|
|
||||||
2659
results/07-jmh-g1.json
Normal file
2659
results/07-jmh-g1.json
Normal file
File diff suppressed because it is too large
Load Diff
1295
results/07-jmh-g1.txt
Normal file
1295
results/07-jmh-g1.txt
Normal file
File diff suppressed because it is too large
Load Diff
2659
results/07-jmh-zgc.json
Normal file
2659
results/07-jmh-zgc.json
Normal file
File diff suppressed because it is too large
Load Diff
1295
results/07-jmh-zgc.txt
Normal file
1295
results/07-jmh-zgc.txt
Normal file
File diff suppressed because it is too large
Load Diff
143
scripts/run-all.ps1
Normal file
143
scripts/run-all.ps1
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compiles and runs the whole ZGC-vs-G1 suite, writing every raw output into results/.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Every number published in the companion blog post comes from this script. It is deliberately
|
||||||
|
boring and sequential: GC benchmarks that run concurrently with each other measure the scheduler.
|
||||||
|
|
||||||
|
.PARAMETER JavaHome
|
||||||
|
Path to a JDK 25 (or newer) installation. Generational ZGC is the only ZGC from JDK 24 onward.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh scripts/run-all.ps1 -JavaHome 'C:\Program Files\Java\jdk-25.0.3'
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string]$JavaHome = $env:JAVA_HOME,
|
||||||
|
[string]$MavenHome = "$HOME\ankurm-blog-tools\apache-maven-3.9.9",
|
||||||
|
[int]$MeasureSeconds = 60,
|
||||||
|
[int]$WarmupSeconds = 20,
|
||||||
|
[switch]$SkipJmh
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if (-not $JavaHome) { throw "Set -JavaHome or JAVA_HOME to a JDK 25+ installation." }
|
||||||
|
|
||||||
|
$Root = Split-Path -Parent $PSScriptRoot
|
||||||
|
$Java = Join-Path $JavaHome 'bin\java.exe'
|
||||||
|
$Javac = Join-Path $JavaHome 'bin\javac.exe'
|
||||||
|
$Out = Join-Path $Root 'out'
|
||||||
|
$Results = Join-Path $Root 'results'
|
||||||
|
$Logs = Join-Path $Root 'logs'
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $Out, $Results, $Logs | Out-Null
|
||||||
|
|
||||||
|
# Fixed heap for every run. -Xms == -Xmx removes heap resizing as a variable; a benchmark that lets
|
||||||
|
# the heap grow during measurement is partly measuring the resize policy.
|
||||||
|
$Heap = @('-Xms2g', '-Xmx2g')
|
||||||
|
|
||||||
|
Write-Host "==> compiling" -ForegroundColor Cyan
|
||||||
|
$sources = Get-ChildItem "$Root\latency\src", "$Root\tuning\src", "$Root\internals\src", "$Root\analysis\src" `
|
||||||
|
-Recurse -Filter *.java -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
|
||||||
|
& $Javac -d $Out @sources
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "compilation failed" }
|
||||||
|
|
||||||
|
# NOTE: several of these commands (notably `java -version` and `-XX:+PrintFlagsFinal`) write to
|
||||||
|
# stderr. PowerShell turns native stderr into error records, and with $ErrorActionPreference='Stop'
|
||||||
|
# that aborts the whole script on a perfectly successful run. Routing through cmd.exe with a plain
|
||||||
|
# 2>&1 merge keeps stderr as text where it belongs.
|
||||||
|
function Invoke-Capture {
|
||||||
|
param([string]$Name, [string[]]$JvmArgs, [string[]]$AppArgs)
|
||||||
|
$file = Join-Path $Results "$Name.txt"
|
||||||
|
Write-Host "==> $Name" -ForegroundColor Cyan
|
||||||
|
$quoted = @("`"$Java`"") + ($JvmArgs + $AppArgs | ForEach-Object {
|
||||||
|
if ($_ -match '[\s]') { "`"$_`"" } else { $_ }
|
||||||
|
})
|
||||||
|
$line = ($quoted -join ' ')
|
||||||
|
$output = cmd /c "$line 2>&1"
|
||||||
|
$output | Out-File -FilePath $file -Encoding utf8
|
||||||
|
$output | Select-Object -Last 6 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||||
|
Write-Host " -> results/$Name.txt" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
# 1. Environment -- printed once per collector so the results file is self-describing.
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
Invoke-Capture -Name '01-env-zgc' -JvmArgs (@('-XX:+UseZGC') + $Heap) -AppArgs @("$Root\env\Env.java")
|
||||||
|
Invoke-Capture -Name '01-env-g1' -JvmArgs (@('-XX:+UseG1GC') + $Heap) -AppArgs @("$Root\env\Env.java")
|
||||||
|
|
||||||
|
Invoke-Capture -Name '02-flags-zgc' -JvmArgs (@('-XX:+UseZGC') + $Heap + @('-XX:+PrintFlagsFinal')) -AppArgs @('-version')
|
||||||
|
Invoke-Capture -Name '02-flags-g1' -JvmArgs (@('-XX:+UseG1GC') + $Heap + @('-XX:+PrintFlagsFinal')) -AppArgs @('-version')
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
# 2. Open-loop latency, with full unified GC logging written alongside.
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
$latencyProps = @("-DwarmupSeconds=$WarmupSeconds", "-Dseconds=$MeasureSeconds")
|
||||||
|
|
||||||
|
Invoke-Capture -Name '03-latency-zgc' `
|
||||||
|
-JvmArgs (@('-XX:+UseZGC') + $Heap + $latencyProps + @("-Xlog:gc*,gc+heap=debug:file=$Logs\zgc-latency.log:time,uptime,level,tags")) `
|
||||||
|
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.latency.LatencyHarness')
|
||||||
|
|
||||||
|
Invoke-Capture -Name '03-latency-g1' `
|
||||||
|
-JvmArgs (@('-XX:+UseG1GC') + $Heap + $latencyProps + @("-Xlog:gc*,gc+heap=debug:file=$Logs\g1-latency.log:time,uptime,level,tags")) `
|
||||||
|
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.latency.LatencyHarness')
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
# 3. Allocation stalls -- deliberately undersized heap. This is the section most GC posts skip.
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
$stallHeap = @('-Xms512m', '-Xmx512m')
|
||||||
|
|
||||||
|
Invoke-Capture -Name '04-stalls-zgc-baseline' `
|
||||||
|
-JvmArgs (@('-XX:+UseZGC') + $stallHeap + @('-Dseconds=20')) `
|
||||||
|
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||||
|
|
||||||
|
Invoke-Capture -Name '04-stalls-zgc-softmax' `
|
||||||
|
-JvmArgs (@('-XX:+UseZGC', '-XX:SoftMaxHeapSize=400m') + $stallHeap + @('-Dseconds=20')) `
|
||||||
|
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||||
|
|
||||||
|
Invoke-Capture -Name '04-stalls-zgc-spiketolerance' `
|
||||||
|
-JvmArgs (@('-XX:+UseZGC', '-XX:ZAllocationSpikeTolerance=5') + $stallHeap + @('-Dseconds=20')) `
|
||||||
|
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||||
|
|
||||||
|
# The same stress under G1: ZGC's stalls become G1's evacuation failures and Full GCs.
|
||||||
|
Invoke-Capture -Name '04-stalls-g1' `
|
||||||
|
-JvmArgs (@('-XX:+UseG1GC') + $stallHeap + @('-Dseconds=20', "-Xlog:gc:file=$Logs\g1-stress.log:time,uptime")) `
|
||||||
|
-AppArgs @('-cp', $Out, 'com.ankurm.zgc.tuning.AllocationStallDemo')
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
# 4. Page-size / humongous behaviour.
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
Invoke-Capture -Name '05-pages-zgc' `
|
||||||
|
-JvmArgs (@('-XX:+UseZGC') + $Heap) -AppArgs @('-cp', $Out, 'com.ankurm.zgc.internals.PageSizeDemo')
|
||||||
|
Invoke-Capture -Name '05-pages-g1' `
|
||||||
|
-JvmArgs (@('-XX:+UseG1GC') + $Heap) -AppArgs @('-cp', $Out, 'com.ankurm.zgc.internals.PageSizeDemo')
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
# 5. GC log analysis -- turns the raw unified logs above into pause distributions.
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
Invoke-Capture -Name '06-gclog-zgc' -JvmArgs @() -AppArgs @('-cp', $Out, 'com.ankurm.zgc.analysis.GcLogParser', "$Logs\zgc-latency.log")
|
||||||
|
Invoke-Capture -Name '06-gclog-g1' -JvmArgs @() -AppArgs @('-cp', $Out, 'com.ankurm.zgc.analysis.GcLogParser', "$Logs\g1-latency.log")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
# 6. JMH throughput / barrier microbenchmarks.
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
if (-not $SkipJmh) {
|
||||||
|
Write-Host "==> building JMH module" -ForegroundColor Cyan
|
||||||
|
Push-Location "$Root\jmh"
|
||||||
|
$env:JAVA_HOME = $JavaHome
|
||||||
|
& (Join-Path $MavenHome 'bin\mvn.cmd') -q -B clean package
|
||||||
|
Pop-Location
|
||||||
|
|
||||||
|
foreach ($gc in @('UseZGC', 'UseG1GC')) {
|
||||||
|
$tag = if ($gc -eq 'UseZGC') { 'zgc' } else { 'g1' }
|
||||||
|
# IMPORTANT: the GC flag must go in -jvmArgs. JMH forks a fresh JVM for every trial, and a
|
||||||
|
# GC flag on the outer `java -jar` command is NOT inherited by the fork. Getting this wrong
|
||||||
|
# is the single most common way to publish a GC benchmark that measured the default GC twice.
|
||||||
|
Invoke-Capture -Name "07-jmh-$tag" -JvmArgs @('-jar', "$Root\jmh\target\benchmarks.jar") `
|
||||||
|
-AppArgs @('-jvmArgs', "-XX:+$gc -Xms2g -Xmx2g", '-prof', 'gc', '-rf', 'json',
|
||||||
|
'-rff', "$Results\07-jmh-$tag.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "All raw output is in $Results" -ForegroundColor Green
|
||||||
89
scripts/run-all.sh
Normal file
89
scripts/run-all.sh
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Linux / macOS equivalent of run-all.ps1.
|
||||||
|
#
|
||||||
|
# ./scripts/run-all.sh /path/to/jdk-25 [measureSeconds] [warmupSeconds]
|
||||||
|
#
|
||||||
|
# Everything published in the companion blog post came from the PowerShell version on Windows;
|
||||||
|
# this script produces the same set of files in results/ so the comparison can be repeated on
|
||||||
|
# server hardware, which is where you should actually make a decision.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
JAVA_HOME_ARG="${1:-${JAVA_HOME:-}}"
|
||||||
|
MEASURE_SECONDS="${2:-60}"
|
||||||
|
WARMUP_SECONDS="${3:-20}"
|
||||||
|
|
||||||
|
if [[ -z "$JAVA_HOME_ARG" ]]; then
|
||||||
|
echo "usage: $0 /path/to/jdk-25 [measureSeconds] [warmupSeconds]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
JAVA="$JAVA_HOME_ARG/bin/java"
|
||||||
|
JAVAC="$JAVA_HOME_ARG/bin/javac"
|
||||||
|
OUT="$ROOT/out"
|
||||||
|
RESULTS="$ROOT/results"
|
||||||
|
LOGS="$ROOT/logs"
|
||||||
|
|
||||||
|
mkdir -p "$OUT" "$RESULTS" "$LOGS"
|
||||||
|
|
||||||
|
# Fixed heap: -Xms == -Xmx removes heap resizing as a variable.
|
||||||
|
HEAP=(-Xms2g -Xmx2g)
|
||||||
|
STALL_HEAP=(-Xms512m -Xmx512m)
|
||||||
|
|
||||||
|
echo "==> compiling"
|
||||||
|
mapfile -t SOURCES < <(find "$ROOT/latency/src" "$ROOT/tuning/src" "$ROOT/internals/src" "$ROOT/analysis/src" -name '*.java')
|
||||||
|
"$JAVAC" -d "$OUT" "${SOURCES[@]}"
|
||||||
|
|
||||||
|
capture() {
|
||||||
|
local name="$1"; shift
|
||||||
|
echo "==> $name"
|
||||||
|
"$@" > "$RESULTS/$name.txt" 2>&1 || true
|
||||||
|
tail -n 4 "$RESULTS/$name.txt" | sed 's/^/ /'
|
||||||
|
}
|
||||||
|
|
||||||
|
capture 01-env-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" "$ROOT/env/Env.java"
|
||||||
|
capture 01-env-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" "$ROOT/env/Env.java"
|
||||||
|
|
||||||
|
capture 02-flags-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" -XX:+PrintFlagsFinal -version
|
||||||
|
capture 02-flags-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" -XX:+PrintFlagsFinal -version
|
||||||
|
|
||||||
|
capture 03-latency-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" \
|
||||||
|
"-DwarmupSeconds=$WARMUP_SECONDS" "-Dseconds=$MEASURE_SECONDS" \
|
||||||
|
"-Xlog:gc*,gc+heap=debug:file=$LOGS/zgc-latency.log:time,uptime,level,tags" \
|
||||||
|
-cp "$OUT" com.ankurm.zgc.latency.LatencyHarness
|
||||||
|
|
||||||
|
capture 03-latency-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" \
|
||||||
|
"-DwarmupSeconds=$WARMUP_SECONDS" "-Dseconds=$MEASURE_SECONDS" \
|
||||||
|
"-Xlog:gc*,gc+heap=debug:file=$LOGS/g1-latency.log:time,uptime,level,tags" \
|
||||||
|
-cp "$OUT" com.ankurm.zgc.latency.LatencyHarness
|
||||||
|
|
||||||
|
capture 04-stalls-zgc-baseline "$JAVA" -XX:+UseZGC "${STALL_HEAP[@]}" -Dseconds=20 \
|
||||||
|
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
|
||||||
|
capture 04-stalls-zgc-softmax "$JAVA" -XX:+UseZGC -XX:SoftMaxHeapSize=400m "${STALL_HEAP[@]}" -Dseconds=20 \
|
||||||
|
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
|
||||||
|
capture 04-stalls-zgc-spiketolerance "$JAVA" -XX:+UseZGC -XX:ZAllocationSpikeTolerance=5 "${STALL_HEAP[@]}" -Dseconds=20 \
|
||||||
|
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
|
||||||
|
capture 04-stalls-g1 "$JAVA" -XX:+UseG1GC "${STALL_HEAP[@]}" -Dseconds=20 \
|
||||||
|
"-Xlog:gc:file=$LOGS/g1-stress.log:time,uptime" \
|
||||||
|
-cp "$OUT" com.ankurm.zgc.tuning.AllocationStallDemo
|
||||||
|
|
||||||
|
capture 05-pages-zgc "$JAVA" -XX:+UseZGC "${HEAP[@]}" -cp "$OUT" com.ankurm.zgc.internals.PageSizeDemo
|
||||||
|
capture 05-pages-g1 "$JAVA" -XX:+UseG1GC "${HEAP[@]}" -cp "$OUT" com.ankurm.zgc.internals.PageSizeDemo
|
||||||
|
|
||||||
|
capture 06-gclog-zgc "$JAVA" -cp "$OUT" com.ankurm.zgc.analysis.GcLogParser "$LOGS/zgc-latency.log"
|
||||||
|
capture 06-gclog-g1 "$JAVA" -cp "$OUT" com.ankurm.zgc.analysis.GcLogParser "$LOGS/g1-latency.log"
|
||||||
|
|
||||||
|
echo "==> building JMH module"
|
||||||
|
(cd "$ROOT/jmh" && JAVA_HOME="$JAVA_HOME_ARG" mvn -q -B clean package)
|
||||||
|
|
||||||
|
# NOTE: the GC flag MUST be inside -jvmArgs. JMH forks a fresh JVM per trial and does not inherit
|
||||||
|
# flags from the outer `java -jar` command. See docs/06-methodology.md section 6.6.
|
||||||
|
for gc in UseZGC UseG1GC; do
|
||||||
|
tag=$([[ "$gc" == "UseZGC" ]] && echo zgc || echo g1)
|
||||||
|
capture "07-jmh-$tag" "$JAVA" -jar "$ROOT/jmh/target/benchmarks.jar" \
|
||||||
|
-jvmArgs "-XX:+$gc -Xms2g -Xmx2g" -prof gc -rf json -rff "$RESULTS/07-jmh-$tag.json"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "All raw output is in $RESULTS"
|
||||||
238
tuning/src/com/ankurm/zgc/tuning/AllocationStallDemo.java
Normal file
238
tuning/src/com/ankurm/zgc/tuning/AllocationStallDemo.java
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package com.ankurm.zgc.tuning;
|
||||||
|
|
||||||
|
import jdk.jfr.Recording;
|
||||||
|
import jdk.jfr.consumer.RecordedEvent;
|
||||||
|
import jdk.jfr.consumer.RecordingFile;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The failure mode nobody screenshots: the <b>ZGC allocation stall</b>.
|
||||||
|
*
|
||||||
|
* <h2>Why this program exists</h2>
|
||||||
|
* Almost every ZGC article ends with a graph of sub-millisecond pause times and an implied
|
||||||
|
* conclusion: "ZGC has no latency problem". The pause times are real. The conclusion is not,
|
||||||
|
* because on ZGC <em>pauses are not where latency comes from</em>.
|
||||||
|
*
|
||||||
|
* <p>ZGC's stop-the-world pauses are O(number of GC roots) and stay in the tens of microseconds
|
||||||
|
* essentially regardless of heap size. What actually hurts a ZGC application is different: when the
|
||||||
|
* application allocates faster than the collector can reclaim, a thread that asks for memory and
|
||||||
|
* cannot be given any is <b>stalled</b> until the collector frees a page. That stall can last tens
|
||||||
|
* or hundreds of milliseconds. It never appears in a pause-time chart, because it is not a pause --
|
||||||
|
* only the allocating thread is blocked, the rest of the JVM keeps running.
|
||||||
|
*
|
||||||
|
* <p>So a ZGC deployment can show a p99.99 pause of 0.2 ms and a p99.99 request latency of 400 ms
|
||||||
|
* at the same time, and both numbers are correct. If you only monitor the first one you will never
|
||||||
|
* find the second one.
|
||||||
|
*
|
||||||
|
* <h2>What this program does</h2>
|
||||||
|
* It starts a JFR recording with {@code jdk.ZAllocationStall} enabled and no threshold (the default
|
||||||
|
* threshold is 10 ms, which hides the smaller ones), then deliberately drives the heap into the
|
||||||
|
* ground: a live set that keeps growing while every thread allocates as fast as it can. Afterwards
|
||||||
|
* it reads its own recording back and prints every stall, with duration and thread.
|
||||||
|
*
|
||||||
|
* <h2>Running it</h2>
|
||||||
|
* A small heap is the point -- do not "fix" it:
|
||||||
|
* <pre>
|
||||||
|
* java -XX:+UseZGC -Xms512m -Xmx512m \
|
||||||
|
* -cp out com.ankurm.zgc.tuning.AllocationStallDemo
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Then try the mitigations and watch the stall count move:
|
||||||
|
* <pre>
|
||||||
|
* -XX:SoftMaxHeapSize=400m # start collecting earlier, before the wall
|
||||||
|
* -XX:ZAllocationSpikeTolerance=5 # assume allocation rate can spike 5x, not 2x
|
||||||
|
* -Xmx1g # the honest fix, when you can afford it
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>On G1 this program produces {@code Evacuation Failure} / long Full GCs instead. G1's
|
||||||
|
* equivalent of a stall is a to-space exhaustion followed by a full, single-threaded-ish
|
||||||
|
* compaction, which <em>is</em> a stop-the-world pause and does show up in pause charts.
|
||||||
|
*/
|
||||||
|
public final class AllocationStallDemo {
|
||||||
|
|
||||||
|
/** How many bytes to keep permanently alive, as a fraction of max heap. */
|
||||||
|
private static final double LIVE_FRACTION =
|
||||||
|
Double.parseDouble(System.getProperty("liveFraction", "0.55"));
|
||||||
|
|
||||||
|
private static final int THREADS =
|
||||||
|
Integer.getInteger("threads", Runtime.getRuntime().availableProcessors());
|
||||||
|
|
||||||
|
private static final int SECONDS = Integer.getInteger("seconds", 20);
|
||||||
|
|
||||||
|
/** Kept in a static field so nothing can be collected while the demo runs. */
|
||||||
|
private static volatile Object[] retained;
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
long maxHeap = Runtime.getRuntime().maxMemory();
|
||||||
|
System.out.printf("max heap : %,d MB%n", maxHeap / 1024 / 1024);
|
||||||
|
System.out.printf("target live set : %,d MB (%.0f%% of heap)%n",
|
||||||
|
(long) (maxHeap * LIVE_FRACTION) / 1024 / 1024, LIVE_FRACTION * 100);
|
||||||
|
System.out.printf("threads : %d, duration: %d s%n%n", THREADS, SECONDS);
|
||||||
|
|
||||||
|
Path jfr = Path.of("allocation-stalls.jfr");
|
||||||
|
|
||||||
|
try (Recording recording = new Recording()) {
|
||||||
|
recording.setName("allocation-stalls");
|
||||||
|
// The default threshold for jdk.ZAllocationStall is 10 ms. Anything shorter is silently
|
||||||
|
// dropped -- which is how a service can be full of 3 ms stalls that no one ever sees.
|
||||||
|
enable(recording, "jdk.ZAllocationStall");
|
||||||
|
enable(recording, "jdk.ZPageAllocation");
|
||||||
|
enable(recording, "jdk.ZYoungGarbageCollection");
|
||||||
|
enable(recording, "jdk.ZOldGarbageCollection");
|
||||||
|
recording.setToDisk(true);
|
||||||
|
recording.setDestination(jfr);
|
||||||
|
recording.start();
|
||||||
|
|
||||||
|
stressHeap();
|
||||||
|
|
||||||
|
recording.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
report(jfr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enables an event if this JVM has it -- ZGC events do not exist when running under G1. */
|
||||||
|
private static void enable(Recording recording, String event) {
|
||||||
|
try {
|
||||||
|
recording.enable(event).withoutThreshold().withStackTrace();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
System.out.println("(event not available on this collector: " + event + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stressHeap() throws Exception {
|
||||||
|
long maxHeap = Runtime.getRuntime().maxMemory();
|
||||||
|
int chunk = 64 * 1024; // 64 KB per retained chunk
|
||||||
|
int retainedSlots = (int) (maxHeap * LIVE_FRACTION / chunk);
|
||||||
|
Object[] live = new Object[retainedSlots];
|
||||||
|
retained = live;
|
||||||
|
|
||||||
|
System.out.println("filling live set ...");
|
||||||
|
for (int i = 0; i < retainedSlots; i++) {
|
||||||
|
live[i] = new byte[chunk];
|
||||||
|
}
|
||||||
|
System.out.printf("live set filled: %,d chunks x %,d KB%n", retainedSlots, chunk / 1024);
|
||||||
|
System.out.println("now allocating hard for " + SECONDS + " s -- expect stalls\n");
|
||||||
|
|
||||||
|
long deadline = System.nanoTime() + Duration.ofSeconds(SECONDS).toNanos();
|
||||||
|
List<Thread> threads = new ArrayList<>();
|
||||||
|
for (int t = 0; t < THREADS; t++) {
|
||||||
|
Thread th = new Thread(() -> {
|
||||||
|
ThreadLocalRandom rnd = ThreadLocalRandom.current();
|
||||||
|
long ops = 0;
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
// Churn: allocate short-lived garbage AND keep replacing live-set entries, so
|
||||||
|
// the collector must both reclaim young pages and relocate old ones.
|
||||||
|
byte[] garbage = new byte[16 * 1024];
|
||||||
|
garbage[0] = 1;
|
||||||
|
if ((ops & 0x3F) == 0) {
|
||||||
|
live[rnd.nextInt(live.length)] = new byte[chunk];
|
||||||
|
}
|
||||||
|
ops++;
|
||||||
|
}
|
||||||
|
}, "allocator-" + t);
|
||||||
|
threads.add(th);
|
||||||
|
th.start();
|
||||||
|
}
|
||||||
|
for (Thread th : threads) th.join();
|
||||||
|
System.out.println("allocation phase done\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void report(Path jfr) throws Exception {
|
||||||
|
if (!Files.exists(jfr)) {
|
||||||
|
System.out.println("no recording produced");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<RecordedEvent> stalls = new ArrayList<>();
|
||||||
|
long youngCycles = 0, oldCycles = 0, pageAllocations = 0;
|
||||||
|
|
||||||
|
try (RecordingFile file = new RecordingFile(jfr)) {
|
||||||
|
while (file.hasMoreEvents()) {
|
||||||
|
RecordedEvent e = file.readEvent();
|
||||||
|
switch (e.getEventType().getName()) {
|
||||||
|
case "jdk.ZAllocationStall" -> stalls.add(e);
|
||||||
|
case "jdk.ZYoungGarbageCollection" -> youngCycles++;
|
||||||
|
case "jdk.ZOldGarbageCollection" -> oldCycles++;
|
||||||
|
case "jdk.ZPageAllocation" -> pageAllocations++;
|
||||||
|
default -> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("=".repeat(72));
|
||||||
|
System.out.println("JFR summary (" + jfr.toAbsolutePath() + ")");
|
||||||
|
System.out.println("=".repeat(72));
|
||||||
|
System.out.printf("young collections : %,d%n", youngCycles);
|
||||||
|
System.out.printf("old collections : %,d%n", oldCycles);
|
||||||
|
System.out.printf("page allocations : %,d%n", pageAllocations);
|
||||||
|
System.out.printf("ALLOCATION STALLS : %,d%n%n", stalls.size());
|
||||||
|
|
||||||
|
if (stalls.isEmpty()) {
|
||||||
|
System.out.println("No stalls recorded. Either the heap was big enough, or you are not");
|
||||||
|
System.out.println("running ZGC. Shrink -Xmx or raise -Dseconds and try again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: RecordedEvent::getDuration is overloaded (no-arg and String-arg), so a method
|
||||||
|
// reference here is ambiguous and will not compile. An explicit lambda pins the overload.
|
||||||
|
stalls.sort(Comparator.comparing((RecordedEvent e) -> e.getDuration()).reversed());
|
||||||
|
long total = 0;
|
||||||
|
for (RecordedEvent e : stalls) total += e.getDuration().toNanos();
|
||||||
|
|
||||||
|
System.out.printf("total time threads spent stalled : %,d ms%n", total / 1_000_000);
|
||||||
|
System.out.printf("mean stall : %,.2f ms%n",
|
||||||
|
total / (double) stalls.size() / 1_000_000);
|
||||||
|
System.out.printf("longest stall : %,.2f ms%n%n",
|
||||||
|
stalls.getFirst().getDuration().toNanos() / 1_000_000.0);
|
||||||
|
|
||||||
|
// Which page size was the thread waiting for? ZGC has three page classes -- Small (2 MB,
|
||||||
|
// for objects up to 256 KB), Medium (32 MB by default, up to 4 MB objects) and Large (one
|
||||||
|
// page per object, for anything bigger). Stalls concentrated on Medium or Large pages mean
|
||||||
|
// something quite different from stalls on Small pages: the first is fragmentation or
|
||||||
|
// large-object pressure, the second is simply "allocating faster than we collect".
|
||||||
|
Map<String, long[]> byPageType = new TreeMap<>(); // type -> {count, totalNanos}
|
||||||
|
for (RecordedEvent e : stalls) {
|
||||||
|
String type = safeString(e, "type");
|
||||||
|
long[] agg = byPageType.computeIfAbsent(type, k -> new long[2]);
|
||||||
|
agg[0]++;
|
||||||
|
agg[1] += e.getDuration().toNanos();
|
||||||
|
}
|
||||||
|
System.out.println("stalls by ZGC page type");
|
||||||
|
byPageType.forEach((type, agg) -> System.out.printf(
|
||||||
|
" %-8s count=%,-8d total=%,7d ms mean=%.2f ms%n",
|
||||||
|
type, agg[0], agg[1] / 1_000_000, agg[1] / (double) agg[0] / 1_000_000));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("ten longest stalls");
|
||||||
|
System.out.printf(" %-11s %-8s %-10s %s%n", "duration", "page", "size", "thread");
|
||||||
|
stalls.stream().limit(10).forEach(e -> System.out.printf(" %8.2f ms %-8s %-10s %s%n",
|
||||||
|
e.getDuration().toNanos() / 1_000_000.0,
|
||||||
|
safeString(e, "type"),
|
||||||
|
safeString(e, "size"),
|
||||||
|
e.getThread() == null ? "?" : e.getThread().getJavaName()));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("Compare against your pause-time chart: none of the above is a pause.");
|
||||||
|
System.out.println("Inspect the raw events, including the stack that was stalled, with:");
|
||||||
|
System.out.println(" jfr print --events ZAllocationStall " + jfr.getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeString(RecordedEvent e, String field) {
|
||||||
|
try {
|
||||||
|
if (!e.hasField(field)) return "-";
|
||||||
|
Object v = e.getValue(field);
|
||||||
|
return v == null ? "-" : String.valueOf(v);
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user