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:
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bytes per instance of the classes that dominate the heap, from GC.class_histogram of the runs made by
|
||||
# footprint.sh (kept in target/hist). The point: which classes shrink, and which do not.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
H="$MOD/target/hist"
|
||||
echo "== bytes per instance (= #bytes / #instances) from jcmd GC.class_histogram after loading 2,000,000 products"
|
||||
printf '%-40s %14s %14s %14s\n' "class" "JDK 26 default" "JDK 27 default" "JDK 27 opt-out"
|
||||
for cls in 'com.ankurm.memory.Product' 'java.util.HashMap$Node' 'java.lang.Long' 'java.lang.String' '[B'; do
|
||||
printf '%-40s' "$cls"
|
||||
for cfg in jdk26-default jdk27-default jdk27-optout; do
|
||||
awk -v c="$cls" '$4==c {printf " %14.1f", $3/$2; found=1} END{if(!found) printf " %14s","?"}' "$H/$cfg.txt"
|
||||
done
|
||||
echo
|
||||
done
|
||||
echo
|
||||
echo "== total bytes of those five classes"
|
||||
for cfg in jdk26-default jdk27-default jdk27-optout; do
|
||||
awk -v cfg="$cfg" '$4=="com.ankurm.memory.Product"||$4=="java.util.HashMap$Node"||$4=="java.lang.Long"||$4=="java.lang.String"||$4=="[B" {s+=$3} END{printf "%-16s %d bytes (%.1f MB)\n", cfg, s, s/1048576}' "$H/$cfg.txt"
|
||||
done
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# The smallest container that can hold the catalog, before and after. For each memory limit the app is started
|
||||
# inside a cgroup with that limit and asked to load N products; the table says what happened.
|
||||
# ok the load finished
|
||||
# java-OOM the JVM threw OutOfMemoryError: the heap (a fraction of the limit) was too small
|
||||
# killed(...) the kernel killed the process: total memory, not just heap, went over the limit
|
||||
# no-result-in-25s the JVM stayed alive but the load (about a second when healthy) did not finish in 25
|
||||
# seconds: the heap is so tight that the collector runs continuously and makes no progress
|
||||
# Each cell is RUNS runs ("3/3 ok" = all three finished); a cell near the boundary can be mixed.
|
||||
# usage: container-sizing.sh [N products] [runs per cell]
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
N="${1:-2000000}"; RUNS="${2:-3}"
|
||||
TMP="$(mktemp -d)"; RETRIES=0
|
||||
MB=$((1024*1024))
|
||||
|
||||
sweep() { # title, jvm-flags-for-all, limits...
|
||||
local title="$1" common="$2"; shift 2
|
||||
echo "== $title"
|
||||
printf '%-8s %-8s %-30s %-30s %-30s\n' "limit" "heap" "JDK 26 default" "JDK 27 default" "JDK 27 opt-out"
|
||||
for lim in "$@"; do
|
||||
local cells=() heap="?"
|
||||
for cfg in "26|$JDK26|" "27|$JDK27|" "27o|$JDK27|-XX:-UseCompactObjectHeaders"; do
|
||||
IFS='|' read -r tag jh fl <<<"$cfg"
|
||||
local ok=0 fails=() rss=()
|
||||
for r in $(seq 1 "$RUNS"); do
|
||||
d="$TMP/$tag-$lim-$r"
|
||||
CG_MEM=$((lim*MB)) CG_CPUS=2 run_app "$jh" "$N" "$d" $common $fl >/dev/null 2>&1
|
||||
h=$(kv "$d/memory-idle.txt" maxHeapMB); [ -n "$h" ] && heap="$h"
|
||||
[ -f "$d/retries" ] && RETRIES=$((RETRIES+1))
|
||||
out=$(cat "$d/outcome.txt")
|
||||
if [ "$out" = ok ]; then ok=$((ok+1)); rss+=("$(kv "$d/memory.txt" rssMB)"); else fails+=("${out%%(*}"); fi
|
||||
done
|
||||
cell="$ok/$RUNS ok"
|
||||
if [ $ok -gt 0 ]; then cell="$cell rss $(printf '%s\n' "${rss[@]}" | median)MB"; fi
|
||||
if [ ${#fails[@]} -gt 0 ]; then cell="$cell [$(printf '%s\n' "${fails[@]}" | sort | uniq -c | awk '{printf "%s%sx %s",sep,$1,$2; sep=" "}')]"; fi
|
||||
cells+=("$cell")
|
||||
done
|
||||
printf '%-8s %-8s %-30s %-30s %-30s\n' "${lim}MB" "${heap}MB" "${cells[0]}" "${cells[1]}" "${cells[2]}"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
# ONLY=75 or ONLY=25 runs one of the two sweeps (each takes about 12 minutes); default is both.
|
||||
ONLY="${ONLY:-both}"
|
||||
echo "# $N products; 2 CPUs; $RUNS runs per cell; rss is the median of the runs that finished"
|
||||
if [ "$ONLY" != 25 ]; then
|
||||
sweep "Heap set to 75% of the container (-XX:MaxRAMPercentage=75)" "-XX:MaxRAMPercentage=75" 400 416 432 448 512 704
|
||||
fi
|
||||
if [ "$ONLY" != 75 ]; then
|
||||
sweep "Heap left at the JVM default (25% of the container)" "" 1024 1152 1216 1280 1536 2048
|
||||
fi
|
||||
echo "# runs that needed a startup retry (the JVM never came up, for a reason other than OOM or a kernel kill): $RETRIES"
|
||||
rm -rf "$TMP"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Where the three JDKs live. Override with environment variables; nothing else is hard-coded.
|
||||
: "${JDK25:=/tmp/tools/j25/jdk-25.0.4.1+1}"
|
||||
: "${JDK26:=$(ls -d /tmp/tools/j26/jdk-26* 2>/dev/null | head -1)}"
|
||||
: "${JDK27:=/tmp/tools/j27/jdk-27+35}"
|
||||
export JDK25 JDK26 JDK27
|
||||
# The runs must not inherit proxy/trust-store options meant for Maven.
|
||||
unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# What each JDK decides by default, and what the flags do. Every line is the JVM's own answer
|
||||
# (java -XX:+PrintFlagsFinal), not something inferred from documentation.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
show() { # label jdk flags... (set CG_MEM/CG_CPUS to run inside a container-style cgroup)
|
||||
local label="$1" jh="$2"; shift 2
|
||||
local out cg=""
|
||||
if [ -n "${CG_MEM:-}" ]; then
|
||||
cg="fl$$-$RANDOM"; cg_create "$cg" "$CG_MEM" "${CG_CPUS:-2}"
|
||||
out=$(bash -c "$CG_JOIN" _ "$cg" "$jh/bin/java" "$@" -XX:+PrintFlagsFinal -version 2>&1)
|
||||
cg_remove "$cg"
|
||||
else
|
||||
out=$("$jh/bin/java" "$@" -XX:+PrintFlagsFinal -version 2>&1)
|
||||
fi
|
||||
if ! echo "$out" | grep -q 'UseCompactObjectHeaders'; then
|
||||
printf '%-52s -> JVM refused to start:\n' "$label"; echo "$out" | head -3 | sed 's/^/ /'; return
|
||||
fi
|
||||
local ch gc heap
|
||||
ch=$(echo "$out" | awk '$2=="UseCompactObjectHeaders"{print $4}')
|
||||
gc=$(echo "$out" | awk '($2=="UseG1GC"||$2=="UseSerialGC"||$2=="UseParallelGC"||$2=="UseZGC") && $4=="true"{print $2}' | tr '\n' ' ')
|
||||
heap=$(echo "$out" | awk '$2=="MaxHeapSize"{printf "%d", $4/1048576}')
|
||||
printf '%-52s -> compact=%-5s collector=%-13s MaxHeapSize=%sMB\n' "$label" "$ch" "$gc" "$heap"
|
||||
echo "$out" | grep -E '^(OpenJDK 64-Bit Server VM warning|Error|Unrecognized)' | sed 's/^/ JVM says: /' | head -2
|
||||
}
|
||||
|
||||
for v in 25 26 27; do
|
||||
jh_var="JDK$v"; jh="${!jh_var}"
|
||||
echo "== JDK $v: $("$jh/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
show "default (this machine: 2 CPUs)" "$jh"
|
||||
show "-XX:+UseCompactObjectHeaders" "$jh" -XX:+UseCompactObjectHeaders
|
||||
show "-XX:-UseCompactObjectHeaders" "$jh" -XX:-UseCompactObjectHeaders
|
||||
echo
|
||||
done
|
||||
|
||||
echo "== The same JDKs in a container: 1 GB memory limit, 1 CPU (a cgroup, set the way Docker sets it)"
|
||||
for v in 25 26 27; do
|
||||
jh_var="JDK$v"; jh="${!jh_var}"
|
||||
CG_MEM=$((1024*1024*1024)) CG_CPUS=1 show "JDK $v, 1 GB, 1 CPU" "$jh"
|
||||
CG_MEM=$((1024*1024*1024)) CG_CPUS=1 show "JDK $v, 1 GB, 1 CPU, MaxRAMPercentage=75" "$jh" -XX:MaxRAMPercentage=75
|
||||
done
|
||||
echo
|
||||
echo "== 1 GB, 2 CPUs"
|
||||
for v in 26 27; do
|
||||
jh_var="JDK$v"; jh="${!jh_var}"
|
||||
CG_MEM=$((1024*1024*1024)) CG_CPUS=2 show "JDK $v, 1 GB, 2 CPUs" "$jh"
|
||||
done
|
||||
echo
|
||||
|
||||
echo "== -XX:MaxRAM: the flag many people used to fake a container limit"
|
||||
show "JDK 26 -XX:MaxRAM=1g" "$JDK26" -XX:MaxRAM=1g
|
||||
show "JDK 27 -XX:MaxRAM=1g" "$JDK27" -XX:MaxRAM=1g
|
||||
echo
|
||||
|
||||
echo "== Compact headers with each collector on JDK 27"
|
||||
for gc in UseG1GC UseParallelGC UseSerialGC UseZGC; do
|
||||
show "JDK 27 -XX:+$gc" "$JDK27" -XX:+$gc
|
||||
done
|
||||
echo
|
||||
|
||||
echo "== Compressed pointers on JDK 27"
|
||||
show "JDK 27 -XX:-UseCompressedClassPointers" "$JDK27" -XX:-UseCompressedClassPointers
|
||||
show "JDK 27 -XX:-UseCompressedOops" "$JDK27" -XX:-UseCompressedOops
|
||||
show "JDK 27 -Xmx32g (a large explicit heap)" "$JDK27" -Xmx32g
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Heap footprint of the same jar on JDK 25, 26 and 27, with compact object headers on and off.
|
||||
# usage: footprint.sh [N products] [runs per configuration]
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
N="${1:-2000000}"; RUNS="${2:-3}"
|
||||
TMP="$(mktemp -d)"
|
||||
MEDS=(); ALL=()
|
||||
|
||||
row() { # label jdk flags...
|
||||
local label="$1" jh="$2"; shift 2
|
||||
local idle=() live=() rss=() gcn=() gcms=() ns=() start=()
|
||||
for r in $(seq 1 "$RUNS"); do
|
||||
run_app "$jh" "$N" "$TMP/$label-$r" -Xms1g -Xmx1g "$@" || { echo "$label run $r FAILED"; return; }
|
||||
local d="$TMP/$label-$r"
|
||||
idle+=("$(kv "$d/memory-idle.txt" heapLiveAfterGcMB)")
|
||||
live+=("$(kv "$d/memory.txt" heapLiveAfterGcMB)")
|
||||
rss+=("$(kv "$d/memory.txt" rssMB)")
|
||||
gcn+=("$(kv "$d/load.txt" gcCountDuringLoad)")
|
||||
gcms+=("$(kv "$d/load.txt" gcMillisDuringLoad)")
|
||||
ns+=("$(kv "$d/lookups.txt" nsPerLookup)")
|
||||
ALL+=("${gcn[-1]} ${rss[-1]} ${ns[-1]}")
|
||||
printf 'run %s/%s idle=%sMB live=%sMB rss=%sMB gcDuringLoad=%s (%sms) lookup=%sns compact=%s\n' "$label" "$r" \
|
||||
"${idle[-1]}" "${live[-1]}" "${rss[-1]}" "${gcn[-1]}" "${gcms[-1]}" "${ns[-1]}" "$(kv "$d/memory.txt" UseCompactObjectHeaders)"
|
||||
done
|
||||
MEDS+=("$(printf 'MED %-22s idle=%sMB live=%sMB rss=%sMB gcDuringLoad=%s gcMs=%s lookupNs=%s' "$label" \
|
||||
"$(printf '%s\n' "${idle[@]}" | median)" "$(printf '%s\n' "${live[@]}" | median)" "$(printf '%s\n' "${rss[@]}" | median)" \
|
||||
"$(printf '%s\n' "${gcn[@]}" | median)" "$(printf '%s\n' "${gcms[@]}" | median)" "$(printf '%s\n' "${ns[@]}" | median)")")
|
||||
echo "${MEDS[-1]}"
|
||||
}
|
||||
|
||||
echo "# $(date -u +%F) ${N} products, -Xms1g -Xmx1g, G1, ${RUNS} runs per configuration; MED lines are medians"
|
||||
echo "# JDK25=$("$JDK25/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
echo "# JDK26=$("$JDK26/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
echo "# JDK27=$("$JDK27/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
row jdk25-default "$JDK25"
|
||||
row jdk25-compact "$JDK25" -XX:+UseCompactObjectHeaders
|
||||
row jdk26-default "$JDK26"
|
||||
row jdk26-compact "$JDK26" -XX:+UseCompactObjectHeaders
|
||||
row jdk27-default "$JDK27"
|
||||
row jdk27-optout "$JDK27" -XX:-UseCompactObjectHeaders
|
||||
echo
|
||||
echo "== medians of $RUNS runs per configuration (idle = live set of the idle app; live = after loading the catalog)"
|
||||
printf '%s\n' "${MEDS[@]}"
|
||||
echo
|
||||
stats() { sort -n | awk '{a[NR]=$1} END{printf "min %d / median %d / max %d", a[1], a[int((NR+1)/2)], a[NR]}'; }
|
||||
echo "== the same runs, grouped by whether a collection ran while the catalog was loading (all configurations together)"
|
||||
for g in zero some; do
|
||||
if [ $g = zero ]; then sel='$1==0'; label="no collection during load"; else sel='$1>0'; label="a collection during load"; fi
|
||||
n=$(printf '%s\n' "${ALL[@]}" | awk "$sel" | wc -l)
|
||||
printf '%-27s runs=%-3s rss MB: %s lookup ns: %s\n' "$label" "$n" \
|
||||
"$(printf '%s\n' "${ALL[@]}" | awk "$sel {print \$2}" | stats)" "$(printf '%s\n' "${ALL[@]}" | awk "$sel {print \$3}" | stats)"
|
||||
done
|
||||
# keep one histogram per JVM for the class-level table
|
||||
mkdir -p "$MOD/target/hist"
|
||||
for c in jdk26-default jdk27-default jdk27-optout; do cp "$TMP/$c-1/histogram.txt" "$MOD/target/hist/$c.txt"; done
|
||||
rm -rf "$TMP"
|
||||
Executable
+96
@@ -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} }'; }
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
cd "$MOD/src/offsets"
|
||||
for cfg in "25||$JDK25" "26||$JDK26" "27||$JDK27" "27|-XX:-UseCompactObjectHeaders|$JDK27"; do
|
||||
IFS='|' read -r v fl jh <<<"$cfg"
|
||||
echo "== JDK $v $fl"
|
||||
"$jh/bin/java" $fl --sun-misc-unsafe-memory-access=allow Offsets.java 2>&1
|
||||
echo
|
||||
done
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# JEP 523 in a small container: 1 CPU, 1 GB. JDK 26 picks Serial here, JDK 27 picks G1. What does the app
|
||||
# cost under each collector, and what does the JVM say it spends outside the heap (Native Memory Tracking)?
|
||||
# usage: one-cpu.sh [N products] [runs]
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
N="${1:-2000000}"; RUNS="${2:-3}"
|
||||
TMP="$(mktemp -d)"
|
||||
MB=$((1024*1024))
|
||||
export NMT=1 CG_MEM=$((1024*MB)) CG_CPUS=1
|
||||
|
||||
nmt() { # file -> "GC committed KB|Total committed KB"
|
||||
local gc tot
|
||||
gc=$(awk '/^- *GC \(reserved/ {gsub(/[^0-9=]/," "); n=split($0,a,"="); print a[3]+0; exit}' "$1")
|
||||
tot=$(awk '/^Total: reserved/ {gsub(/[^0-9=]/," "); n=split($0,a,"="); print a[3]+0; exit}' "$1")
|
||||
echo "$gc|$tot"
|
||||
}
|
||||
|
||||
row() { # label jdk flags...
|
||||
local label="$1" jh="$2"; shift 2
|
||||
local st=() live=() rss=() gcn=() gcms=() ld=() gck=() totk=() coll="" cpus=""
|
||||
for r in $(seq 1 "$RUNS"); do
|
||||
d="$TMP/$label-$r"
|
||||
run_app "$jh" "$N" "$d" -XX:MaxRAMPercentage=50 "$@" >/dev/null 2>&1 || { echo "$label run $r FAILED: $(cat "$d/outcome.txt")"; return; }
|
||||
coll=$(kv "$d/memory.txt" collectors); cpus=$(kv "$d/memory.txt" availableProcessors)
|
||||
st+=("$(grep -o '[0-9.]*' "$d/startup.txt" | tail -1)")
|
||||
live+=("$(kv "$d/memory.txt" heapLiveAfterGcMB)"); rss+=("$(kv "$d/memory.txt" rssMB)")
|
||||
gcn+=("$(kv "$d/load.txt" gcCountDuringLoad)"); gcms+=("$(kv "$d/load.txt" gcMillisDuringLoad)")
|
||||
ld+=("$(kv "$d/load.txt" loadMillis)")
|
||||
IFS='|' read -r g t <<<"$(nmt "$d/nmt.txt")"; gck+=("$g"); totk+=("$t")
|
||||
done
|
||||
m() { printf '%s\n' "$@" | median; }
|
||||
printf '%s\n collectors=%s cpus=%s\n' "== $label" "$coll" "$cpus"
|
||||
printf ' startup=%ss live=%sMB rss=%sMB loadMillis=%s gcDuringLoad=%s (%sms)\n' \
|
||||
"$(m "${st[@]}")" "$(m "${live[@]}")" "$(m "${rss[@]}")" "$(m "${ld[@]}")" "$(m "${gcn[@]}")" "$(m "${gcms[@]}")"
|
||||
printf ' NMT: GC committed=%s KB Total committed=%s KB\n\n' "$(m "${gck[@]}")" "$(m "${totk[@]}")"
|
||||
}
|
||||
|
||||
echo "# $N products, container = 1 GB / 1 CPU (cgroup), -XX:MaxRAMPercentage=50 (512 MB heap), NMT summary on, medians of $RUNS runs"
|
||||
row "jdk26-default (the JVM chose)" "$JDK26"
|
||||
row "jdk26 -XX:+UseG1GC" "$JDK26" -XX:+UseG1GC
|
||||
row "jdk27-default (the JVM chose)" "$JDK27"
|
||||
row "jdk27 -XX:+UseSerialGC" "$JDK27" -XX:+UseSerialGC
|
||||
row "jdk27 -XX:+UseSerialGC, compact off" "$JDK27" -XX:+UseSerialGC -XX:-UseCompactObjectHeaders
|
||||
rm -rf "$TMP"
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# What does "killed" mean in the container sweep? Runs the edge case (JDK 27, compact headers off, 400 MB
|
||||
# container, heap 75%) a few times and reads the cgroup's own counters and the kernel log afterwards, so the
|
||||
# claim "the kernel's memory controller killed it" rests on the kernel's say-so and not on an exit code.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
KILLED=()
|
||||
echo "# JDK 27, -XX:-UseCompactObjectHeaders -XX:MaxRAMPercentage=75, container limit 400 MB, 2 CPUs; 2,000,000 products"
|
||||
for attempt in 1 2 3 4; do
|
||||
cg="oomchk$attempt"
|
||||
cg_create "$cg" $((400*1024*1024)) 2
|
||||
port=$((30000 + RANDOM % 5000))
|
||||
bash -c "$CG_JOIN" _ "$cg" "$JDK27/bin/java" -XX:MaxRAMPercentage=75 -XX:-UseCompactObjectHeaders -jar "$JAR" --server.port=$port >/dev/null 2>&1 &
|
||||
pid=$!
|
||||
for _ in $(seq 1 120); do curl -sf localhost:$port/ping >/dev/null 2>&1 && break; sleep 0.25; done
|
||||
resp=$(curl -s --max-time 25 -o /dev/null -w '%{http_code}' "localhost:$port/load?n=2000000")
|
||||
sleep 1
|
||||
if kill -0 $pid 2>/dev/null; then state="process still alive"; else wait $pid 2>/dev/null; state="process gone, exit $?"; KILLED+=("$pid"); fi
|
||||
echo "== attempt $attempt: /load answered HTTP $resp; $state"
|
||||
echo " memory.failcnt=$(cat $CGROOT/memory/$cg/memory.failcnt) memory.max_usage=$(( $(cat $CGROOT/memory/$cg/memory.max_usage_in_bytes) / 1048576 ))MB limit=$(( $(cat $CGROOT/memory/$cg/memory.limit_in_bytes) / 1048576 ))MB $(grep '^oom_kill ' $CGROOT/memory/$cg/memory.oom_control)"
|
||||
kill $pid 2>/dev/null; wait $pid 2>/dev/null
|
||||
cg_remove "$cg"
|
||||
echo
|
||||
done
|
||||
echo "== the kernel log (dmesg): what it says about the processes that disappeared above"
|
||||
for p in "${KILLED[@]:-}"; do
|
||||
[ -n "$p" ] && dmesg 2>/dev/null | grep "Memory cgroup out of memory: Killed process $p " | sed 's/^\[[ 0-9.]*\] //'
|
||||
done
|
||||
exit 0
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# A transcript of the service's own endpoints on JDK 27, so the post can show what a request and the
|
||||
# /memory report look like. Port 18081 is fixed so the transcript reads the same every time.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
PORT=18081
|
||||
"$JDK27/bin/java" -Xms1g -Xmx1g -jar "$JAR" --server.port=$PORT >/dev/null 2>&1 &
|
||||
PID=$!
|
||||
for _ in $(seq 1 200); do curl -sf "localhost:$PORT/ping" >/dev/null 2>&1 && break; sleep 0.25; done
|
||||
for path in "/memory" "/load?n=2000000" "/memory" "/products/42" "/products/2000000"; do
|
||||
echo "\$ curl -s -w ' [HTTP %{http_code}]\n' localhost:$PORT$path"
|
||||
curl -s -w ' [HTTP %{http_code}]\n' "localhost:$PORT$path" | sed 's/^ \[HTTP/[HTTP/'
|
||||
echo
|
||||
done
|
||||
kill $PID 2>/dev/null; wait $PID 2>/dev/null
|
||||
exit 0
|
||||
Reference in New Issue
Block a user