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:
2026-10-12 17:30:55 +00:00
co-authored by Claude Sonnet 5.5
parent fc05eed4b5
commit d565be93c6
13 changed files with 388 additions and 1 deletions
+1 -1
View File
@@ -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),
`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
+70
View File
@@ -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.
+16
View File
@@ -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}
+48
View File
@@ -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}
+4
View File
@@ -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
+12
View File
@@ -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
+33
View File
@@ -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>
+42
View File
@@ -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);
}
}
+15
View File
@@ -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
+35
View File
@@ -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
+20
View File
@@ -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
+9
View File
@@ -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;
}
}
}