Add jdk27-perf: the same Spring Boot service measured on JDK 25 and JDK 27
Live heap, throughput, p99 and startup across four configurations, including opt-in and opt-out of compact object headers. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WzVQ6zFh9cuLPj9RSbRH8U
This commit is contained in:
@@ -17,7 +17,7 @@ ThreadLocal (JEP 506)**. Every number and transcript in the post comes from a fi
|
|||||||
|
|
||||||
Next LTS: Java 29, September 2027 (Oracle Java SE Support Roadmap). Layout: `g1-container/`, `object-headers/`, `jep-tour/` (27 features and `broken/` 26-era sources),
|
Next LTS: Java 29, September 2027 (Oracle Java SE Support Roadmap). Layout: `g1-container/`, `object-headers/`, `jep-tour/` (27 features and `broken/` 26-era sources),
|
||||||
`recap26/` (the Java 26 lane), `lanes/` (the 21 to 25 hub demos, the AI prompts in `lanes/prompts/` and the build-file demo in `lanes/upgrade/`), `other-changes/`, `api-diff/`,
|
`recap26/` (the Java 26 lane), `lanes/` (the 21 to 25 hub demos, the AI prompts in `lanes/prompts/` and the build-file demo in `lanes/upgrade/`), `other-changes/`, `api-diff/`,
|
||||||
`gatherers/` (JEP 485, standalone post), `scoped-values/` (JEP 506 vs `ThreadLocal`, standalone post).
|
`gatherers/` (JEP 485, standalone post), `scoped-values/` (JEP 506 vs `ThreadLocal`, standalone post). `jdk27-perf/` (the same Spring Boot service on JDK 25 and 27: footprint, throughput, p99; its README and `output/` replace a `docs/` folder).
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# jdk27-perf — what actually got faster in JDK 27, measured on one Spring Boot service
|
||||||
|
|
||||||
|
Companion module for the ankurm.com post **What Actually Got Faster in JDK 27: Measured on a Spring Boot App**
|
||||||
|
(<https://ankurm.com/what-actually-got-faster-in-jdk-27-measured-spring-boot/>).
|
||||||
|
No `docs/` folder: this README and the raw files in [`output/`](output) are the documentation.
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
JDK 27 changed two defaults that matter to a server: compact object headers are on (JEP 534) and G1 is the default collector everywhere (JEP 523).
|
||||||
|
Does the same Spring Boot service get faster, smaller, or neither when you move from JDK 25 to JDK 27 with no flags?
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
| Thing | Value |
|
||||||
|
|---|---|
|
||||||
|
| JDK 25 | Temurin 25.0.4.1+1 (LTS) |
|
||||||
|
| JDK 27 | Temurin 27+35 (GA 2026-09-15) |
|
||||||
|
| Spring Boot | 4.1.1, `spring-boot-starter-webmvc` |
|
||||||
|
| Machine | 2 vCPU, 8 GB RAM Linux sandbox (the load client shares the box, so absolute numbers are modest and noisy) |
|
||||||
|
| Service | 3,000,000 resident `Item(int id, int qty)` records; `/quote?n=200` builds 200 response records; Jackson serialises them |
|
||||||
|
| Load | closed loop, 4 client threads, 15 s warmup discarded, 30 s measured, client always JDK 25 |
|
||||||
|
| Repetitions | 3 per configuration, interleaved so machine drift hits all four equally |
|
||||||
|
|
||||||
|
Four configurations: `jdk25 defaults`, `jdk25 +CompactObjectHeaders` (opt in), `jdk27 defaults`, `jdk27 -CompactObjectHeaders` (opt out).
|
||||||
|
[`output/01-defaults.txt`](output/01-defaults.txt) shows both JDKs pick G1 on this machine, so the collector is the same everywhere and only headers and the rest of the JDK differ.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```console
|
||||||
|
mvn -B -DskipTests package # build with JDK 25 or later
|
||||||
|
export J25=/path/to/jdk-25 J27=/path/to/jdk-27
|
||||||
|
scripts/run-all.sh # about 12 minutes; regenerates output/
|
||||||
|
REPS=5 scripts/run-all.sh # more repetitions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
Medians of 3 runs ([`output/03-summary.txt`](output/03-summary.txt), raw blocks in [`output/02-runs.txt`](output/02-runs.txt)):
|
||||||
|
|
||||||
|
| Config | Startup ms | Throughput req/s (range) | p99 ms |
|
||||||
|
|---|---|---|---|
|
||||||
|
| jdk25 defaults | 3437 | 6788 (6239-6868) | 3.23 |
|
||||||
|
| jdk25 +CompactObjectHeaders | 3512 | 7157 (6407-7228) | 3.09 |
|
||||||
|
| jdk27 defaults | 3220 | 7016 (6755-7275) | 2.99 |
|
||||||
|
| jdk27 -CompactObjectHeaders | 3388 | 7010 (6867-7227) | 3.03 |
|
||||||
|
|
||||||
|
Resident data set after a forced full GC ([`output/04-live-set.txt`](output/04-live-set.txt), identical in all three repetitions):
|
||||||
|
|
||||||
|
| Config | Live heap MiB |
|
||||||
|
|---|---|
|
||||||
|
| jdk25 defaults | 93 |
|
||||||
|
| jdk25 +CompactObjectHeaders | 69 |
|
||||||
|
| jdk27 defaults | 70 |
|
||||||
|
| jdk27 -CompactObjectHeaders | 93-94 |
|
||||||
|
|
||||||
|
## What it says
|
||||||
|
|
||||||
|
- **Footprint is the clear win.** Compact headers cut the live heap by 23 to 24 MiB (whole-MiB rounding). That matches 3,000,000 objects times 8 bytes saved, which is 22.9 MiB. A two-int record goes from 24 to 16 bytes.
|
||||||
|
- **Speed is not distinguishable.** The throughput ranges overlap in every row and p99 differs by fractions of a millisecond. On this box I cannot claim JDK 27 is faster.
|
||||||
|
- **Startup is inconclusive.** JDK 27 defaults had the lowest median, but the individual runs overlap with JDK 25.
|
||||||
|
- Resident set size and committed heap vary run to run with G1's sizing decisions; see the per-run `/stats` lines in `02-runs.txt` before drawing a conclusion from them.
|
||||||
|
|
||||||
|
## Honest notes
|
||||||
|
|
||||||
|
- `output/02-runs.txt` was produced by the first build of the jar. The `/live` endpoint used for `04-live-set.txt` was added afterwards; it does not touch the `/quote` path, but strictly the two files come from two builds of this module.
|
||||||
|
- Timings drift. Treat them as indicative.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
MIT, as the parent repository.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
== JDK 25
|
||||||
|
openjdk version "25.0.4.1" 2026-08-18 LTS
|
||||||
|
size_t MaxHeapSize = 1568669696 {product} {ergonomic}
|
||||||
|
bool UseCompactObjectHeaders = false {product lp64_product} {default}
|
||||||
|
bool UseG1GC = true {product} {ergonomic}
|
||||||
|
bool UseSerialGC = false {product} {default}
|
||||||
|
bool UseZGC = false {product} {default}
|
||||||
|
|
||||||
|
== JDK 27
|
||||||
|
openjdk version "27" 2026-09-15
|
||||||
|
size_t MaxHeapSize = 1568669696 {product} {ergonomic}
|
||||||
|
bool UseCompactObjectHeaders = true {product lp64_product} {default}
|
||||||
|
bool UseG1GC = true {product} {ergonomic}
|
||||||
|
bool UseSerialGC = false {product} {default}
|
||||||
|
bool UseZGC = false {product} {default}
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
== jdk25 defaults
|
||||||
|
startup_ms=3437
|
||||||
|
requests=203628 throughput=6788 req/s p50=0.45 ms p99=3.23 ms p99.9=7.27 ms max=27.80 ms
|
||||||
|
{"javaVersion":"25.0.4.1","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"false","heapUsedMiB":162,"heapCommittedMiB":252,"rssMiB":403}
|
||||||
|
== jdk25 +CompactObjectHeaders
|
||||||
|
startup_ms=3658
|
||||||
|
requests=214723 throughput=7157 req/s p50=0.43 ms p99=2.88 ms p99.9=6.60 ms max=31.57 ms
|
||||||
|
{"javaVersion":"25.0.4.1","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"true","heapUsedMiB":114,"heapCommittedMiB":219,"rssMiB":361}
|
||||||
|
== jdk27 defaults
|
||||||
|
startup_ms=3166
|
||||||
|
requests=218262 throughput=7275 req/s p50=0.44 ms p99=2.76 ms p99.9=6.49 ms max=20.31 ms
|
||||||
|
{"javaVersion":"27","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"true","heapUsedMiB":76,"heapCommittedMiB":284,"rssMiB":434}
|
||||||
|
== jdk27 -CompactObjectHeaders
|
||||||
|
startup_ms=3363
|
||||||
|
requests=216804 throughput=7227 req/s p50=0.43 ms p99=3.00 ms p99.9=6.46 ms max=15.04 ms
|
||||||
|
{"javaVersion":"27","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"false","heapUsedMiB":155,"heapCommittedMiB":310,"rssMiB":458}
|
||||||
|
== jdk25 defaults
|
||||||
|
startup_ms=3563
|
||||||
|
requests=187160 throughput=6239 req/s p50=0.46 ms p99=4.36 ms p99.9=8.83 ms max=35.12 ms
|
||||||
|
{"javaVersion":"25.0.4.1","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"false","heapUsedMiB":108,"heapCommittedMiB":177,"rssMiB":325}
|
||||||
|
== jdk25 +CompactObjectHeaders
|
||||||
|
startup_ms=3512
|
||||||
|
requests=216853 throughput=7228 req/s p50=0.43 ms p99=3.09 ms p99.9=6.54 ms max=20.85 ms
|
||||||
|
{"javaVersion":"25.0.4.1","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"true","heapUsedMiB":158,"heapCommittedMiB":228,"rssMiB":363}
|
||||||
|
== jdk27 defaults
|
||||||
|
startup_ms=3220
|
||||||
|
requests=202647 throughput=6755 req/s p50=0.46 ms p99=3.20 ms p99.9=7.43 ms max=24.21 ms
|
||||||
|
{"javaVersion":"27","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"true","heapUsedMiB":238,"heapCommittedMiB":289,"rssMiB":432}
|
||||||
|
== jdk27 -CompactObjectHeaders
|
||||||
|
startup_ms=3539
|
||||||
|
requests=206016 throughput=6867 req/s p50=0.45 ms p99=3.21 ms p99.9=6.98 ms max=54.74 ms
|
||||||
|
{"javaVersion":"27","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"false","heapUsedMiB":189,"heapCommittedMiB":313,"rssMiB":454}
|
||||||
|
== jdk25 defaults
|
||||||
|
startup_ms=3419
|
||||||
|
requests=206034 throughput=6868 req/s p50=0.45 ms p99=3.14 ms p99.9=6.58 ms max=21.56 ms
|
||||||
|
{"javaVersion":"25.0.4.1","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"false","heapUsedMiB":204,"heapCommittedMiB":273,"rssMiB":415}
|
||||||
|
== jdk25 +CompactObjectHeaders
|
||||||
|
startup_ms=3433
|
||||||
|
requests=192205 throughput=6407 req/s p50=0.47 ms p99=3.75 ms p99.9=7.58 ms max=22.22 ms
|
||||||
|
{"javaVersion":"25.0.4.1","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"true","heapUsedMiB":87,"heapCommittedMiB":154,"rssMiB":313}
|
||||||
|
== jdk27 defaults
|
||||||
|
startup_ms=3602
|
||||||
|
requests=210490 throughput=7016 req/s p50=0.45 ms p99=2.99 ms p99.9=6.26 ms max=16.33 ms
|
||||||
|
{"javaVersion":"27","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"true","heapUsedMiB":128,"heapCommittedMiB":229,"rssMiB":386}
|
||||||
|
== jdk27 -CompactObjectHeaders
|
||||||
|
startup_ms=3388
|
||||||
|
requests=210293 throughput=7010 req/s p50=0.45 ms p99=3.03 ms p99.9=6.68 ms max=32.18 ms
|
||||||
|
{"javaVersion":"27","gc":["G1 Young Generation","G1 Concurrent GC","G1 Old Generation"],"compactHeaders":"false","heapUsedMiB":227,"heapCommittedMiB":311,"rssMiB":475}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
jdk25 +CompactObjectHeaders | startup 3512 ms | throughput 7157 req/s | p99 3.09 ms | heap used 114 MiB | RSS 361 MiB
|
||||||
|
jdk25 defaults | startup 3437 ms | throughput 6788 req/s | p99 3.23 ms | heap used 162 MiB | RSS 403 MiB
|
||||||
|
jdk27 -CompactObjectHeaders | startup 3388 ms | throughput 7010 req/s | p99 3.03 ms | heap used 189 MiB | RSS 458 MiB
|
||||||
|
jdk27 defaults | startup 3220 ms | throughput 7016 req/s | p99 2.99 ms | heap used 128 MiB | RSS 432 MiB
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
jdk25 defaults: live heap after full GC 93 MiB
|
||||||
|
jdk25 +CompactObjectHeaders: live heap after full GC 69 MiB
|
||||||
|
jdk27 defaults: live heap after full GC 70 MiB
|
||||||
|
jdk27 -CompactObjectHeaders: live heap after full GC 93 MiB
|
||||||
|
jdk25 defaults: live heap after full GC 93 MiB
|
||||||
|
jdk25 +CompactObjectHeaders: live heap after full GC 69 MiB
|
||||||
|
jdk27 defaults: live heap after full GC 70 MiB
|
||||||
|
jdk27 -CompactObjectHeaders: live heap after full GC 94 MiB
|
||||||
|
jdk25 defaults: live heap after full GC 93 MiB
|
||||||
|
jdk25 +CompactObjectHeaders: live heap after full GC 69 MiB
|
||||||
|
jdk27 defaults: live heap after full GC 70 MiB
|
||||||
|
jdk27 -CompactObjectHeaders: live heap after full GC 94 MiB
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>4.1.1</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>jdk27-perf</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>jdk27-perf</name>
|
||||||
|
<description>Same Spring Boot service measured on JDK 25 and JDK 27</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.*;
|
||||||
|
|
||||||
|
/** Closed-loop load: THREADS threads, WARMUP seconds discarded, then MEASURE seconds recorded. */
|
||||||
|
public class Load {
|
||||||
|
public static void main(String[] a) throws Exception {
|
||||||
|
int port = Integer.parseInt(a[0]), warmup = Integer.parseInt(a[1]), measure = Integer.parseInt(a[2]), threads = Integer.parseInt(a[3]);
|
||||||
|
HttpClient c = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
|
||||||
|
HttpRequest rq = HttpRequest.newBuilder(URI.create("http://localhost:" + port + "/quote?n=200")).build();
|
||||||
|
long start = System.nanoTime();
|
||||||
|
long measureFrom = start + warmup * 1_000_000_000L, end = measureFrom + measure * 1_000_000_000L;
|
||||||
|
List<long[]> perThread = new CopyOnWriteArrayList<>();
|
||||||
|
ExecutorService ex = Executors.newFixedThreadPool(threads);
|
||||||
|
List<Future<?>> fs = new ArrayList<>();
|
||||||
|
for (int t = 0; t < threads; t++) fs.add(ex.submit(() -> {
|
||||||
|
long[] buf = new long[2_000_000]; int n = 0;
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
long s = System.nanoTime();
|
||||||
|
if (s >= end) break;
|
||||||
|
c.send(rq, HttpResponse.BodyHandlers.discarding());
|
||||||
|
long e = System.nanoTime();
|
||||||
|
if (s >= measureFrom && n < buf.length) buf[n++] = e - s;
|
||||||
|
}
|
||||||
|
} catch (Exception e) { throw new RuntimeException(e); }
|
||||||
|
perThread.add(Arrays.copyOf(buf, n));
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
for (Future<?> f : fs) f.get();
|
||||||
|
ex.shutdown();
|
||||||
|
long total = 0; for (long[] b : perThread) total += b.length;
|
||||||
|
long[] all = new long[(int) total]; int p = 0;
|
||||||
|
for (long[] b : perThread) { System.arraycopy(b, 0, all, p, b.length); p += b.length; }
|
||||||
|
Arrays.sort(all);
|
||||||
|
System.out.printf("requests=%d throughput=%.0f req/s p50=%.2f ms p99=%.2f ms p99.9=%.2f ms max=%.2f ms%n",
|
||||||
|
total, total / (double) measure, all[(int) (total * 0.50)] / 1e6, all[(int) (total * 0.99)] / 1e6,
|
||||||
|
all[(int) (total * 0.999)] / 1e6, all[(int) total - 1] / 1e6);
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Usage: live-set.sh <java-home> <label> [jvm args...]
|
||||||
|
# Starts the server, forces a full GC through /live, and prints the heap still in use.
|
||||||
|
# This is the resident data set: 3,000,000 two-int records plus the Spring Boot baseline.
|
||||||
|
set -u
|
||||||
|
JH=$1; LABEL=$2; shift 2
|
||||||
|
JAR="$(dirname "$0")/../target/jdk27-perf-1.0.0.jar"
|
||||||
|
PORT=${PORT:-18082}
|
||||||
|
"$JH/bin/java" "$@" -jar "$JAR" --server.port=$PORT --spring.main.banner-mode=off \
|
||||||
|
--logging.level.root=WARN > /dev/null 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
until [ "$(curl -s -o /dev/null -m 2 -w '%{http_code}' "localhost:$PORT/stats")" = 200 ]; do sleep 0.05; done
|
||||||
|
live=$(curl -s "localhost:$PORT/live" | sed -n 's/.*"liveHeapMiB":\([0-9]*\).*/\1/p')
|
||||||
|
echo "$LABEL: live heap after full GC ${live} MiB"
|
||||||
|
kill $PID 2>/dev/null; wait $PID 2>/dev/null
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates output/. Set J25 and J27. REPS defaults to 3. Configs are interleaved per repetition
|
||||||
|
# so slow drift on the machine hits every configuration equally. Timings are indicative only.
|
||||||
|
set -u
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
OUT="$HERE/../output"; mkdir -p "$OUT"
|
||||||
|
: "${J25:?}" "${J27:?}"
|
||||||
|
export JDK_CLIENT="$J25"
|
||||||
|
REPS=${REPS:-3}
|
||||||
|
{
|
||||||
|
for p in "25|$J25" "27|$J27"; do
|
||||||
|
v=${p%%|*}; h=${p#*|}
|
||||||
|
echo "== JDK $v"
|
||||||
|
"$h/bin/java" -version 2>&1 | grep -v Picked | sed -n 1p
|
||||||
|
"$h/bin/java" -XX:+PrintFlagsFinal -version 2>&1 | grep -E ' (UseCompactObjectHeaders|UseG1GC|UseSerialGC|UseZGC|MaxHeapSize) ' | sed 's/ \{2,\}/ /g'
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
} > "$OUT/01-defaults.txt"
|
||||||
|
: > "$OUT/02-runs.txt"
|
||||||
|
for r in $(seq 1 "$REPS"); do
|
||||||
|
"$HERE/run-one.sh" "$J25" "jdk25 defaults" >> "$OUT/02-runs.txt" 2>&1
|
||||||
|
"$HERE/run-one.sh" "$J25" "jdk25 +CompactObjectHeaders" -XX:+UseCompactObjectHeaders >> "$OUT/02-runs.txt" 2>&1
|
||||||
|
"$HERE/run-one.sh" "$J27" "jdk27 defaults" >> "$OUT/02-runs.txt" 2>&1
|
||||||
|
"$HERE/run-one.sh" "$J27" "jdk27 -CompactObjectHeaders" -XX:-UseCompactObjectHeaders >> "$OUT/02-runs.txt" 2>&1
|
||||||
|
done
|
||||||
|
"$HERE/summarize.sh" "$OUT/02-runs.txt" > "$OUT/03-summary.txt"
|
||||||
|
# 04 - resident data set after a forced full GC (this is where compact headers show up)
|
||||||
|
{
|
||||||
|
for r in 1 2 3; do
|
||||||
|
"$HERE/live-set.sh" "$J25" "jdk25 defaults"
|
||||||
|
"$HERE/live-set.sh" "$J25" "jdk25 +CompactObjectHeaders" -XX:+UseCompactObjectHeaders
|
||||||
|
"$HERE/live-set.sh" "$J27" "jdk27 defaults"
|
||||||
|
"$HERE/live-set.sh" "$J27" "jdk27 -CompactObjectHeaders" -XX:-UseCompactObjectHeaders
|
||||||
|
done
|
||||||
|
} > "$OUT/04-live-set.txt" 2>&1
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Usage: run-one.sh <java-home> <label> [jvm args...]
|
||||||
|
# One cold start, then 15 s warmup + 30 s measured load, then a /stats snapshot. One result block.
|
||||||
|
set -u
|
||||||
|
JH=$1; LABEL=$2; shift 2
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
JAR="$HERE/../target/jdk27-perf-1.0.0.jar"
|
||||||
|
PORT=${PORT:-18081}
|
||||||
|
CLIENT=${JDK_CLIENT:-$JH}
|
||||||
|
t0=$(date +%s%N)
|
||||||
|
"$JH/bin/java" "$@" -jar "$JAR" --server.port=$PORT --spring.main.banner-mode=off \
|
||||||
|
--logging.level.root=WARN > /dev/null 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
until [ "$(curl -s -o /dev/null -m 2 -w '%{http_code}' "localhost:$PORT/stats")" = 200 ]; do sleep 0.02; done
|
||||||
|
t1=$(date +%s%N)
|
||||||
|
echo "== $LABEL"
|
||||||
|
echo "startup_ms=$(( (t1 - t0) / 1000000 ))"
|
||||||
|
"$CLIENT/bin/java" "$HERE/Load.java" $PORT 15 30 ${THREADS:-4} 2>&1 | grep -v Picked
|
||||||
|
curl -s "localhost:$PORT/stats"; echo
|
||||||
|
kill $PID 2>/dev/null; wait $PID 2>/dev/null
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Usage: summarize.sh output/02-runs.txt -> median of each metric per label
|
||||||
|
awk '
|
||||||
|
/^== /{label=substr($0,4); next}
|
||||||
|
/^startup_ms=/{split($0,a,"="); st[label]=st[label] " " a[2]}
|
||||||
|
/^requests=/{for(i=1;i<=NF;i++){split($i,kv,"="); if(kv[1]=="throughput")tp[label]=tp[label] " " kv[2]; if(kv[1]=="p99")p99[label]=p99[label] " " kv[2]}}
|
||||||
|
/^\{"javaVersion"/{match($0,/"heapUsedMiB":[0-9]+/); h=substr($0,RSTART+14,RLENGTH-14); hu[label]=hu[label] " " h; match($0,/"rssMiB":[0-9]+/); r=substr($0,RSTART+9,RLENGTH-9); rss[label]=rss[label] " " r}
|
||||||
|
function med(s, n,arr,i,j,t){n=split(s,arr," "); for(i=1;i<=n;i++)for(j=i+1;j<=n;j++)if(arr[j]+0<arr[i]+0){t=arr[i];arr[i]=arr[j];arr[j]=t}; return arr[int((n+1)/2)]}
|
||||||
|
END{for(l in st) printf "%s | startup %s ms | throughput %s req/s | p99 %s ms | heap used %s MiB | RSS %s MiB\n", l, med(st[l]), med(tp[l]), med(p99[l]), med(hu[l]), med(rss[l])}' "$1" | sort
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.ankurm.perf;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import com.sun.management.HotSpotDiagnosticMXBean;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service with a realistic shape: a large resident data set of small objects (so object
|
||||||
|
* headers matter) and a request path that allocates and serialises (so the GC and JIT matter).
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
public class PerfApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(PerfApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Two ints: 8 bytes of fields. With classic headers that is a 24-byte object, with compact headers 16. */
|
||||||
|
record Item(int id, int qty) {}
|
||||||
|
|
||||||
|
record Line(int id, int qty, long total) {}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
static class Api {
|
||||||
|
private static final int ITEMS = 3_000_000;
|
||||||
|
private final Item[] catalogue = new Item[ITEMS];
|
||||||
|
|
||||||
|
Api() {
|
||||||
|
for (int i = 0; i < ITEMS; i++) catalogue[i] = new Item(i, i % 7 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds n lines from random catalogue entries; Jackson serialises the result. */
|
||||||
|
@GetMapping("/quote")
|
||||||
|
List<Line> quote(@RequestParam(defaultValue = "200") int n) {
|
||||||
|
ThreadLocalRandom r = ThreadLocalRandom.current();
|
||||||
|
List<Line> out = new ArrayList<>(n);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
Item it = catalogue[r.nextInt(ITEMS)];
|
||||||
|
out.add(new Line(it.id(), it.qty(), (long) it.id() * it.qty()));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forces a full collection, then reports what is still in use: the resident data set. */
|
||||||
|
@GetMapping("/live")
|
||||||
|
Map<String, Object> live() throws Exception {
|
||||||
|
System.gc();
|
||||||
|
Thread.sleep(500);
|
||||||
|
Runtime rt = Runtime.getRuntime();
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("liveHeapMiB", (rt.totalMemory() - rt.freeMemory()) >> 20);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stats")
|
||||||
|
Map<String, Object> stats() throws Exception {
|
||||||
|
Runtime rt = Runtime.getRuntime();
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("javaVersion", System.getProperty("java.version"));
|
||||||
|
m.put("gc", ManagementFactory.getGarbageCollectorMXBeans().stream().map(b -> b.getName()).toList());
|
||||||
|
m.put("compactHeaders", ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
|
||||||
|
.getVMOption("UseCompactObjectHeaders").getValue());
|
||||||
|
m.put("heapUsedMiB", (rt.totalMemory() - rt.freeMemory()) >> 20);
|
||||||
|
m.put("heapCommittedMiB", rt.totalMemory() >> 20);
|
||||||
|
long rssKb = 0;
|
||||||
|
for (String l : Files.readAllLines(Path.of("/proc/self/status")))
|
||||||
|
if (l.startsWith("VmRSS:")) rssKb = Long.parseLong(l.replaceAll("\\D+", ""));
|
||||||
|
m.put("rssMiB", rssKb >> 10);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user