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
+19
View File
@@ -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
+53
View File
@@ -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"
+8
View File
@@ -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
+64
View File
@@ -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
+56
View File
@@ -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"
+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} }'; }
+9
View File
@@ -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
+44
View File
@@ -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"
+28
View File
@@ -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
+15
View File
@@ -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