Files
asmhatreandClaude Sonnet 5 2f88734f5d Add flags: production JVM flag cheat sheet for Spring Boot on JDK 25/27
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]>
2026-10-01 04:34:04 +00:00

6.5 KiB

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:MaxRAMPercentage explicitly; do not rely on the 25% default for a web service
  • Never set an absolute -Xmx/-Xms in a container unless it is below the memory limit with room to spare
  • Never set -XX:-UseContainerSupport, and never set -XX:ActiveProcessorCount above the real CPU quota
  • Leave -XX:+HeapDumpOnOutOfMemoryError (with -XX:HeapDumpPath pointed at a mounted volume) and -Xlog:gc* on in production
  • Delete /internal/jvm-flags (or guard it) before shipping

License

MIT — see LICENSE.