MaxRAMPercentage vs a hardcoded -Xmx, -XX:-UseContainerSupport, the ActiveProcessorCount lie, and which diagnostic flags are worth leaving on in prod, verified in real Docker containers on JDK 25 and JDK 27. Co-Authored-By: Claude Sonnet 5 <[email protected]>
flags — companion module for "JVM Flags for Spring Boot in Containers (JDK 25/27)"
Module flags/ of the zgc-jdk25-benchmarks
repository. Companion code for the post
JVM Flags for Spring Boot in Containers (JDK 25/27): The Production Cheat Sheet
on ankurm.com.
Everything here was run in real Docker containers with real --memory and --cpus
limits — not read about. docs/output/*.txt is the committed, unedited transcript of
every run the post quotes from; scripts/run-all.sh reproduces all of it.
This module is the flag-by-flag reference. Two sibling modules already go deeper on
specific pieces of this topic and this module leans on them rather than repeating them:
../jdk27-memory/ measured compact object headers and G1-everywhere
on a 2,000,000-row Spring Boot service across JDK 25/26/27, with a full container-sizing
sweep and kernel-level OOM forensics (dmesg, memory.failcnt) — see its
README and the post
JDK 27 Memory Changes.
The kubernetes-deployment/ module of
spring-boot-demo
measured CPU limits throttling the garbage collector and the ActiveProcessorCount overshoot
anti-pattern in full, on a real cluster — see the post
Deploying Spring Boot 4 on Kubernetes.
Versions this was verified against
| Component | Version |
|---|---|
| JDK (LTS) | Temurin 25.0.4.1+1 |
| JDK (latest feature release) | Temurin 27+35 (GA 2026-09-15) |
| Spring Boot | 4.1.1 |
| Spring Framework | 7.0.x (via Boot 4.1.1 BOM) |
| Docker Engine | 29.4.3 |
| Base images | eclipse-temurin:25-jre, eclipse-temurin:25-jdk, ubuntu:24.04 (JDK 27 has no official Temurin image yet — see docker/Dockerfile.jdk27) |
JEPs this module exercises directly: JEP 523 (G1 default everywhere, JDK 27), JEP 534 (compact object headers by default, JDK 27), JEP 519 (compact object headers as a full product feature, JDK 25).
Quickstart
Requires Docker (a running daemon) and JDK 25+ to build.
cd flags
mvn -q -B clean package -DskipTests
cp target/flags-demo.jar docker/flags-demo.jar
# JDK 27 has no eclipse-temurin:27-* image yet, so pull the GA tarball once:
curl -sL -o docker/temurin27.tar.gz \
"https://api.adoptium.net/v3/binary/latest/27/ga/linux/x64/jdk/hotspot/normal/eclipse"
docker build -t flags-demo:jdk25 -f docker/Dockerfile.jdk25 docker
docker build -t flags-demo:jdk25-jdk -f docker/Dockerfile.jdk25-jdk docker # full JDK, needed for jcmd
docker build -t flags-demo:jdk27 -f docker/Dockerfile.jdk27 docker
# run the Spring Boot app itself, container-limited to 512MB / 1 CPU
docker run --rm -p 8080:8080 --memory=512m --cpus=1 \
-e JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75" \
flags-demo:jdk27
curl localhost:8080/internal/jvm-flags | python3 -m json.tool
scripts/run-all.sh regenerates every file in docs/output/.
What's in this module
| Path | What it is |
|---|---|
src/main/java/.../FlagsDemoApplication.java |
The Spring Boot app |
src/main/java/.../JvmDiagnosticsController.java |
/internal/jvm-flags — prints what the JVM actually resolved its container-relevant flags to, live. Delete before shipping. |
tools/MemoryHog.java |
Standalone (non-Spring) allocator used to deliberately trigger OOM conditions under different flag combinations — compiled to docker/memoryhog.jar |
tools/HeaderSizeDemo.java |
Standalone allocator used with jcmd GC.class_histogram to measure the real per-instance byte cost of compact object headers — compiled to docker/headersize.jar |
docker/Dockerfile.jdk25 |
eclipse-temurin:25-jre + the app jar |
docker/Dockerfile.jdk25-jdk |
eclipse-temurin:25-jdk + the app jar (full JDK, for jcmd-based experiments) |
docker/Dockerfile.jdk27 |
ubuntu:24.04 + the Temurin 27 GA tarball installed by hand + the app jar |
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /internal/jvm-flags |
Live JSON: resolved MaxRAMPercentage/MaxHeapSize/GC/compact-header flags, heap usage, GC bean names, raw JVM input arguments |
| GET | /actuator/health |
Standard Spring Boot Actuator health |
Captured output index
| File | Demonstrates |
|---|---|
docs/output/01-gc-selection-jdk25.txt |
JDK 25 picks Serial below ~1 CPU / 1.8GB, G1 above it |
docs/output/02-gc-selection-jdk27.txt |
JDK 27 picks G1 unconditionally — JEP 523 |
docs/output/03-activeprocessorcount-lie.txt |
Overriding ActiveProcessorCount above the real CPU quota inflates ParallelGCThreads to match |
docs/output/04-compact-object-headers.txt |
Real jcmd GC.class_histogram byte counts: 24 bytes/instance with the legacy header, 16 bytes/instance compact — JEP 534 |
docs/output/05-maxrampercentage-correct-oom.txt |
-XX:MaxRAMPercentage=75 in a 256MB container: heap fills, JVM throws a catchable OutOfMemoryError, container survives |
docs/output/06-hardcoded-xmx-oomkilled.txt |
-Xmx1g in a 256MB container: no JVM exception, the kernel OOM-killer kills the container (exit 137) |
docs/output/07-usecontainersupport-false-oomkilled.txt |
-XX:-UseContainerSupport makes the JVM size its heap off the host's memory, then the container is killed the same way |
docs/output/08-heapdumponoutofmemoryerror.txt |
-XX:+HeapDumpOnOutOfMemoryError actually leaving a usable .hprof behind |
docs/output/09-native-memory-tracking.txt |
-XX:NativeMemoryTracking=summary showing what the 25%/75% heap percentage does not account for |
Production checklist (expanded in the post's closing accordion)
- Set
-XX:MaxRAMPercentageexplicitly; do not rely on the 25% default for a web service - Never set an absolute
-Xmx/-Xmsin a container unless it is below the memory limit with room to spare - Never set
-XX:-UseContainerSupport, and never set-XX:ActiveProcessorCountabove the real CPU quota - Leave
-XX:+HeapDumpOnOutOfMemoryError(with-XX:HeapDumpPathpointed at a mounted volume) and-Xlog:gc*on in production - Delete
/internal/jvm-flags(or guard it) before shipping
License
MIT — see LICENSE.