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:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user