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]>
112 lines
6.5 KiB
Markdown
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`.
|