Add jdk27-memory: compact object headers and G1-everywhere on a Spring Boot service

Spring Boot 4.1.1 catalog service measured on JDK 25, 26 and 27: live heap 320 MB to 273 MB with compact headers, per-class bytes per instance, container-sizing sweeps with real cgroup limits, 1-CPU Serial vs G1 with Native Memory Tracking, removed flags (MaxRAM, UseCompressedClassPointers), and a kernel OOM-kill check. Outputs in jdk27-memory/output.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
Claude
2026-09-24 15:09:55 +00:00
parent e189da79ba
commit e83c9949d8
29 changed files with 1041 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Shared helpers. Source this; do not run it.
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MOD="$(cd "$HERE/.." && pwd)"
source "$HERE/env.sh"
JAR="$MOD/target/catalog.jar"
# ---- containers -----------------------------------------------------------------------------------------
# There is no Docker daemon in the sandbox, so limits are set the way Docker sets them: a cgroup with a
# memory limit and a CPU quota. The JVM's container support reads exactly those files (cgroup v1 here).
CGROOT=/sys/fs/cgroup
cg_create() { # NAME MEMORY_BYTES CPUS
mkdir -p "$CGROOT/memory/$1" "$CGROOT/cpu/$1"
echo "$2" >"$CGROOT/memory/$1/memory.limit_in_bytes"
echo 100000 >"$CGROOT/cpu/$1/cpu.cfs_period_us"
echo $(( $3 * 100000 )) >"$CGROOT/cpu/$1/cpu.cfs_quota_us"
}
cg_remove() { rmdir "$CGROOT/memory/$1" "$CGROOT/cpu/$1" 2>/dev/null; }
# In run_app the JVM is started as: bash -c '<join the cgroup>; exec java ...' &
# so that $! is the JVM's own pid (a function wrapper would make $! the wrapper's pid).
CG_JOIN='n=$1; shift; echo $$ >'"$CGROOT"'/memory/$n/cgroup.procs; echo $$ >'"$CGROOT"'/cpu/$n/cgroup.procs; exec "$@"'
# run_app JAVA_HOME N OUTDIR [jvm flags...]
# Starts the catalog on that JVM, loads N products, and leaves these files in OUTDIR:
# startup.txt memory-idle.txt load.txt memory.txt lookups.txt histogram.txt outcome.txt
# Set CG_MEM (bytes) and CG_CPUS to run inside a container-style cgroup. Set NMT=1 for native memory tracking.
# Returns 1 if the load did not complete; outcome.txt says why: java-OOM, killed (kernel OOM kill),
# no-result-in-25s (the JVM is alive but collecting garbage without making progress; a healthy load takes
# about a second), or error.
run_app_once() {
local jh="$1" n="$2" out="$3"; shift 3
local port=$((20000 + RANDOM % 20000))
local cg=""
mkdir -p "$out"
local extra=()
if [ -n "${NMT:-}" ]; then extra+=(-XX:NativeMemoryTracking=summary); fi
if [ -n "${CG_MEM:-}" ]; then
cg="jm$$-$RANDOM"; cg_create "$cg" "$CG_MEM" "${CG_CPUS:-2}"
bash -c "$CG_JOIN" _ "$cg" "$jh/bin/java" "${extra[@]}" "$@" -jar "$JAR" --server.port=$port >"$out/app.log" 2>&1 &
else
"$jh/bin/java" "${extra[@]}" "$@" -jar "$JAR" --server.port=$port >"$out/app.log" 2>&1 &
fi
local pid=$!
local up=0
for _ in $(seq 1 400); do
kill -0 $pid 2>/dev/null || break
if curl -sf "localhost:$port/ping" >/dev/null 2>&1; then up=1; break; fi
sleep 0.25
done
local rc=0
if [ $up = 0 ]; then
wait $pid 2>/dev/null; local st=$?
if grep -q OutOfMemoryError "$out/app.log"; then echo "java-OOM-at-startup" >"$out/outcome.txt"
elif [ $st -ge 128 ]; then echo "killed(exit $st)" >"$out/outcome.txt"; else echo "error(exit $st)" >"$out/outcome.txt"; fi
: >"$out/startup.txt"; [ -n "$cg" ] && cg_remove "$cg"; return 1
fi
grep -o 'Started MemoryApplication in [0-9.]* seconds' "$out/app.log" >"$out/startup.txt"
curl -s --max-time 60 "localhost:$port/memory" >"$out/memory-idle.txt"
curl -s --max-time 25 "localhost:$port/load?n=$n" >"$out/load.txt"; local crc=$?
if ! grep -q '^loaded=' "$out/load.txt"; then
rc=1
if [ $crc -eq 28 ]; then echo "no-result-in-25s" >"$out/outcome.txt"
elif ! kill -0 $pid 2>/dev/null; then wait $pid 2>/dev/null; echo "killed(exit $?)" >"$out/outcome.txt"
elif grep -q OutOfMemoryError "$out/app.log" "$out/load.txt"; then echo "java-OOM" >"$out/outcome.txt"
else echo "error" >"$out/outcome.txt"; fi
else
curl -s --max-time 60 "localhost:$port/memory" >"$out/memory.txt"
curl -s --max-time 60 "localhost:$port/lookups?n=2000000" >"$out/lookups.txt"
"$jh/bin/jcmd" $pid GC.class_histogram >"$out/histogram.txt" 2>&1
if [ -n "${NMT:-}" ]; then "$jh/bin/jcmd" $pid VM.native_memory summary >"$out/nmt.txt" 2>&1; fi
if kill -0 $pid 2>/dev/null; then echo "ok" >"$out/outcome.txt"; else wait $pid 2>/dev/null; echo "killed(exit $?)" >"$out/outcome.txt"; rc=1; fi
fi
kill $pid 2>/dev/null; wait $pid 2>/dev/null
[ -n "$cg" ] && cg_remove "$cg"
return $rc
}
# run_app: run_app_once, but a run whose JVM never came up for a reason that is neither OutOfMemoryError
# nor a kernel kill (in practice: the random port was taken) is retried, and the retry is counted in
# $out/retries so that a summary can say so instead of hiding it.
run_app() {
local out="$3" tries=0
while :; do
run_app_once "$@"; local rc=$?
if [ $rc -ne 0 ] && grep -q '^error(exit' "$out/outcome.txt" 2>/dev/null && [ ! -s "$out/startup.txt" ] && [ $tries -lt 2 ]; then
tries=$((tries+1)); echo $tries >"$out/retries"; continue
fi
return $rc
done
}
# kv FILE KEY -> value
kv() { grep "^$2=" "$1" | head -1 | cut -d= -f2-; }
# median of numbers on stdin
median() { sort -n | awk '{a[NR]=$1} END{ if(NR%2){print a[(NR+1)/2]} else {printf "%d\n",(a[NR/2]+a[NR/2+1])/2} }'; }