# flags — companion module for "JVM Flags for Spring Boot in Containers (JDK 25/27)" Module `flags/` of the [`zgc-jdk25-benchmarks`](https://ankurm.com/git.app/asmhatre/zgc-jdk25-benchmarks) repository. Companion code for the post [JVM Flags for Spring Boot in Containers (JDK 25/27): The Production Cheat Sheet](https://ankurm.com/jvm-flags-for-spring-boot-in-containers-jdk-25-27/) 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/`](../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](../jdk27-memory/README.md) and the post [JDK 27 Memory Changes](https://ankurm.com/jdk-27-compact-object-headers-g1-default-memory-spring-boot/). The `kubernetes-deployment/` module of [`spring-boot-demo`](https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/kubernetes-deployment) 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](https://ankurm.com/spring-boot-4-kubernetes-probes-graceful-shutdown-cpu-limits-hpa/). ## 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](https://openjdk.org/jeps/523) (G1 default everywhere, JDK 27), [JEP 534](https://openjdk.org/jeps/534) (compact object headers by default, JDK 27), [JEP 519](https://openjdk.org/jeps/519) (compact object headers as a full product feature, JDK 25). ## Quickstart Requires Docker (a running daemon) and JDK 25+ to build. ```bash 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](https://openjdk.org/jeps/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](https://openjdk.org/jeps/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`.