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

112 lines
6.5 KiB
Markdown

# 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`.