From 2f88734f5d7f71d24fb9af8a1b2db95b339b5246 Mon Sep 17 00:00:00 2001 From: Ankur Mhatre Date: Thu, 1 Oct 2026 04:34:04 +0000 Subject: [PATCH] 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 --- README.md | 1 + flags/.gitignore | 8 ++ flags/README.md | 111 ++++++++++++++++++ flags/docker/Dockerfile.jdk25 | 5 + flags/docker/Dockerfile.jdk25-jdk | 5 + flags/docker/Dockerfile.jdk27 | 12 ++ flags/docs/output/01-gc-selection-jdk25.txt | 59 ++++++++++ flags/docs/output/02-gc-selection-jdk27.txt | 59 ++++++++++ .../output/03-activeprocessorcount-lie.txt | 13 ++ .../docs/output/04-compact-object-headers.txt | 27 +++++ .../05-maxrampercentage-correct-oom.txt | 23 ++++ .../output/06-hardcoded-xmx-oomkilled.txt | 26 ++++ ...07-usecontainersupport-false-oomkilled.txt | 31 +++++ .../output/08-heapdumponoutofmemoryerror.txt | 23 ++++ .../docs/output/09-native-memory-tracking.txt | 97 +++++++++++++++ flags/pom.xml | 44 +++++++ flags/scripts/run-all.sh | 107 +++++++++++++++++ .../flagsdemo/FlagsDemoApplication.java | 19 +++ .../flagsdemo/JvmDiagnosticsController.java | 83 +++++++++++++ flags/src/main/resources/application.yml | 16 +++ flags/tools/HeaderSizeDemo.java | 26 ++++ flags/tools/MemoryHog.java | 37 ++++++ 22 files changed, 832 insertions(+) create mode 100644 flags/.gitignore create mode 100644 flags/README.md create mode 100644 flags/docker/Dockerfile.jdk25 create mode 100644 flags/docker/Dockerfile.jdk25-jdk create mode 100644 flags/docker/Dockerfile.jdk27 create mode 100644 flags/docs/output/01-gc-selection-jdk25.txt create mode 100644 flags/docs/output/02-gc-selection-jdk27.txt create mode 100644 flags/docs/output/03-activeprocessorcount-lie.txt create mode 100644 flags/docs/output/04-compact-object-headers.txt create mode 100644 flags/docs/output/05-maxrampercentage-correct-oom.txt create mode 100644 flags/docs/output/06-hardcoded-xmx-oomkilled.txt create mode 100644 flags/docs/output/07-usecontainersupport-false-oomkilled.txt create mode 100644 flags/docs/output/08-heapdumponoutofmemoryerror.txt create mode 100644 flags/docs/output/09-native-memory-tracking.txt create mode 100644 flags/pom.xml create mode 100755 flags/scripts/run-all.sh create mode 100644 flags/src/main/java/com/ankurm/flagsdemo/FlagsDemoApplication.java create mode 100644 flags/src/main/java/com/ankurm/flagsdemo/JvmDiagnosticsController.java create mode 100644 flags/src/main/resources/application.yml create mode 100644 flags/tools/HeaderSizeDemo.java create mode 100644 flags/tools/MemoryHog.java diff --git a/README.md b/README.md index 411a9d7..09a6897 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ the part most "ZGC is faster" posts leave out. | [`analysis/GcLogParser.java`](analysis/src/com/ankurm/zgc/analysis/GcLogParser.java) | Unified-log parser that keeps pauses and concurrent phases apart | none | | [`jmh/`](jmh/src/main/java/com/ankurm/zgc/jmh) | Throughput and barrier microbenchmarks | JMH 1.37 | | [`jdk27-memory/`](jdk27-memory) | Spring Boot 4.1 service measured on JDK 25, 26 and 27: compact object headers (JEP 534) and G1 everywhere (JEP 523), heap footprint, container sizing with real cgroup limits. Its README and `output/` replace a `docs/` folder; the write-up is [the post](https://ankurm.com/jdk-27-compact-object-headers-g1-default-memory-spring-boot/) | Spring Boot 4.1.1, three JDKs | +| [`flags/`](flags) | The flag-by-flag production cheat sheet: `MaxRAMPercentage` vs a hardcoded `-Xmx` (one fails safely, one gets the container SIGKILLed), `-XX:-UseContainerSupport`, the `ActiveProcessorCount` lie, and which diagnostic flags (`HeapDumpOnOutOfMemoryError`, `NativeMemoryTracking`) are worth leaving on in prod — on real Docker containers, JDK 25 and JDK 27. Its README and `docs/output/` replace a `docs/` folder; the write-up is [the post](https://ankurm.com/jvm-flags-for-spring-boot-in-containers-jdk-25-27/) | Spring Boot 4.1.1, Docker, two JDKs | ### JMH benchmarks diff --git a/flags/.gitignore b/flags/.gitignore new file mode 100644 index 0000000..9144bd9 --- /dev/null +++ b/flags/.gitignore @@ -0,0 +1,8 @@ +target/ +docker/temurin27.tar.gz +docker/flags-demo.jar +docker/memoryhog.jar +docker/headersize.jar +*.hprof +*.class +raw/ diff --git a/flags/README.md b/flags/README.md new file mode 100644 index 0000000..2d5cc69 --- /dev/null +++ b/flags/README.md @@ -0,0 +1,111 @@ +# 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`. diff --git a/flags/docker/Dockerfile.jdk25 b/flags/docker/Dockerfile.jdk25 new file mode 100644 index 0000000..ce9e63a --- /dev/null +++ b/flags/docker/Dockerfile.jdk25 @@ -0,0 +1,5 @@ +FROM eclipse-temurin:25-jre +WORKDIR /app +COPY flags-demo.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/flags/docker/Dockerfile.jdk25-jdk b/flags/docker/Dockerfile.jdk25-jdk new file mode 100644 index 0000000..fe945c6 --- /dev/null +++ b/flags/docker/Dockerfile.jdk25-jdk @@ -0,0 +1,5 @@ +FROM eclipse-temurin:25-jdk +WORKDIR /app +COPY flags-demo.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/flags/docker/Dockerfile.jdk27 b/flags/docker/Dockerfile.jdk27 new file mode 100644 index 0000000..f894d99 --- /dev/null +++ b/flags/docker/Dockerfile.jdk27 @@ -0,0 +1,12 @@ +# No eclipse-temurin:27-* tag exists on Docker Hub yet (checked 2026-10-01) so this +# installs the Adoptium Temurin 27 GA tarball directly onto a slim Ubuntu base. +FROM ubuntu:24.04 +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* +COPY temurin27.tar.gz /tmp/temurin27.tar.gz +RUN mkdir -p /opt/java && tar -xzf /tmp/temurin27.tar.gz -C /opt/java --strip-components=1 && rm /tmp/temurin27.tar.gz +ENV JAVA_HOME=/opt/java +ENV PATH=$JAVA_HOME/bin:$PATH +WORKDIR /app +COPY flags-demo.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/flags/docs/output/01-gc-selection-jdk25.txt b/flags/docs/output/01-gc-selection-jdk25.txt new file mode 100644 index 0000000..9d2ee76 --- /dev/null +++ b/flags/docs/output/01-gc-selection-jdk25.txt @@ -0,0 +1,59 @@ +$ for mem,cpu in 256m:1 512m:1 1800m:1 512m:2 1800m:2 4g:2; do + docker run --rm --memory=$mem --cpus=$cpu --entrypoint java flags-demo:jdk25 \ + -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseSerialGC|UseCompactObjectHeaders|MaxHeapSize|MaxRAMPercentage" + done + +--- mem=256m cpus=1 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 132120576 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = false {product lp64_product} {default} + bool UseG1GC = false {product} {default} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = true {product} {ergonomic} + +--- mem=512m cpus=1 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 134217728 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = false {product lp64_product} {default} + bool UseG1GC = false {product} {default} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = true {product} {ergonomic} + +--- mem=1800m cpus=1 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 471859200 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = false {product lp64_product} {default} + bool UseG1GC = false {product} {default} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = true {product} {ergonomic} + +--- mem=512m cpus=2 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 134217728 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = false {product lp64_product} {default} + bool UseG1GC = false {product} {default} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = true {product} {ergonomic} + +--- mem=1800m cpus=2 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 471859200 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = false {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + +--- mem=4g cpus=2 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 1073741824 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = false {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + diff --git a/flags/docs/output/02-gc-selection-jdk27.txt b/flags/docs/output/02-gc-selection-jdk27.txt new file mode 100644 index 0000000..5952da5 --- /dev/null +++ b/flags/docs/output/02-gc-selection-jdk27.txt @@ -0,0 +1,59 @@ +$ for mem,cpu in 256m:1 512m:1 1800m:1 512m:2 1800m:2 4g:2; do + docker run --rm --memory=$mem --cpus=$cpu --entrypoint java flags-demo:jdk27 \ + -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseSerialGC|UseCompactObjectHeaders|MaxHeapSize|MaxRAMPercentage" + done + +--- mem=256m cpus=1 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 132120576 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = true {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + +--- mem=512m cpus=1 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 134217728 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = true {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + +--- mem=1800m cpus=1 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 471859200 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = true {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + +--- mem=512m cpus=2 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 134217728 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = true {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + +--- mem=1800m cpus=2 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 471859200 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = true {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + +--- mem=4g cpus=2 --- + int ActiveProcessorCount = -1 {product} {default} + size_t MaxHeapSize = 1073741824 {product} {ergonomic} + double MaxRAMPercentage = 25.000000 {product} {default} + bool UseCompactObjectHeaders = true {product lp64_product} {default} + bool UseG1GC = true {product} {ergonomic} + bool UseParallelGC = false {product} {default} + bool UseSerialGC = false {product} {default} + diff --git a/flags/docs/output/03-activeprocessorcount-lie.txt b/flags/docs/output/03-activeprocessorcount-lie.txt new file mode 100644 index 0000000..47962b7 --- /dev/null +++ b/flags/docs/output/03-activeprocessorcount-lie.txt @@ -0,0 +1,13 @@ +$ docker run --rm --cpus=1 --memory=1g --entrypoint java flags-demo:jdk27 -XX:+PrintFlagsFinal -version | grep -E "ParallelGCThreads|ConcGCThreads|ActiveProcessorCount" +$ docker run --rm --cpus=1 --memory=1g --entrypoint java flags-demo:jdk27 -XX:ActiveProcessorCount=8 -XX:+PrintFlagsFinal -version | grep -E "ParallelGCThreads|ConcGCThreads|ActiveProcessorCount" + +=== ActiveProcessorCount lying to the JVM: 1 real CPU, told there are 8 === +--- JDK27, --cpus=1, no override (correct) --- + int ActiveProcessorCount = -1 {product} {default} + uint ConcGCThreads = 1 {product} {ergonomic} + uint ParallelGCThreads = 1 {product} {default} + +--- JDK27, --cpus=1, -XX:ActiveProcessorCount=8 (lying) --- + int ActiveProcessorCount = 8 {product} {command line} + uint ConcGCThreads = 2 {product} {ergonomic} + uint ParallelGCThreads = 8 {product} {default} diff --git a/flags/docs/output/04-compact-object-headers.txt b/flags/docs/output/04-compact-object-headers.txt new file mode 100644 index 0000000..aa6c0c2 --- /dev/null +++ b/flags/docs/output/04-compact-object-headers.txt @@ -0,0 +1,27 @@ +$ docker run -d --memory=1g --name hdr -v $PWD/docker/headersize.jar:/headersize.jar \ + --entrypoint java flags-demo: [extra flags] -cp /headersize.jar HeaderSizeDemo 2000000 +$ docker exec hdr jcmd 1 GC.class_histogram | grep -E "Pair|Total" + +--- JDK 25, default (UseCompactObjectHeaders=false), flags-demo:jdk25-jdk --- + 1: 2000000 48000000 HeaderSizeDemo$Pair + 273: 1 24 java.lang.invoke.StringConcatFactory$InlineHiddenClassStrategy$MethodHandlePair (java.base@25.0.4.1) +Total 2030279 57368512 + +--- JDK 25, -XX:+UseCompactObjectHeaders --- + 1: 2000000 32000000 HeaderSizeDemo$Pair + 300: 1 16 java.lang.invoke.StringConcatFactory$InlineHiddenClassStrategy$MethodHandlePair (java.base@25.0.4.1) +Total 2030283 41272056 + +--- JDK 27, default (UseCompactObjectHeaders=true), flags-demo:jdk27 --- + 1: 2000000 32000000 HeaderSizeDemo$Pair + 317: 1 16 java.lang.invoke.StringConcatFactory$InlineHiddenClassStrategy$MethodHandlePair (java.base@27) +Total 2030937 41320144 + +--- JDK 27, -XX:-UseCompactObjectHeaders --- + 1: 2000000 48000000 HeaderSizeDemo$Pair + 282: 1 24 java.lang.invoke.StringConcatFactory$InlineHiddenClassStrategy$MethodHandlePair (java.base@27) +Total 2031092 57425040 + +2,000,000 instances of a 2-int object: 48,000,000 bytes (24 bytes each) with the old header, +32,000,000 bytes (16 bytes each) compact. 33% smaller per instance, reproduced symmetrically +in both directions on both JDKs. diff --git a/flags/docs/output/05-maxrampercentage-correct-oom.txt b/flags/docs/output/05-maxrampercentage-correct-oom.txt new file mode 100644 index 0000000..987b0b3 --- /dev/null +++ b/flags/docs/output/05-maxrampercentage-correct-oom.txt @@ -0,0 +1,23 @@ +$ docker run --rm --memory=256m --entrypoint java flags-demo:jdk25 \ + -Xlog:gc -XX:MaxRAMPercentage=75 -cp /memoryhog.jar MemoryHog 24 200 + +[0.004s][info][gc] Using Serial +MemoryHog starting: chunk=24MB sleep=200ms +[0.048s][info][gc] GC(0) Pause Young (Allocation Failure) 1M->1M(7M) 0.713ms +held=24MB heapUsed=25MB heapMax=185MB freeHeap=6MB +[0.291s][info][gc] GC(1) Pause Young (Allocation Failure) 25M->25M(31M) 1.105ms +held=48MB heapUsed=49MB heapMax=185MB freeHeap=6MB +[0.507s][info][gc] GC(2) Pause Young (Allocation Failure) 49M->49M(77M) 0.643ms +held=72MB heapUsed=73MB heapMax=185MB freeHeap=27MB +[0.723s][info][gc] GC(3) Pause Young (Allocation Failure) 73M->73M(112M) 0.176ms +held=96MB heapUsed=98MB heapMax=185MB freeHeap=14MB +[0.962s][info][gc] GC(4) Pause Young (Allocation Failure) 98M->97M(147M) 23.535ms +held=120MB heapUsed=122MB heapMax=185MB freeHeap=24MB +[1.201s][info][gc] GC(5) Pause Young (Allocation Failure) 122M->121M(181M) 28.449ms +held=144MB heapUsed=146MB heapMax=185MB freeHeap=35MB +held=168MB heapUsed=170MB heapMax=185MB freeHeap=11MB +[1.633s][info][gc] GC(6) Pause Full (Allocation Failure) 170M->169M(185M) 5.895ms +[1.649s][info][gc] GC(7) Pause Full (Allocation Failure) 169M->169M(185M) 15.822ms +Exception in thread "main" java.lang.OutOfMemoryError: Java heap space + at MemoryHog.main(MemoryHog.java:21) +exit=1 diff --git a/flags/docs/output/06-hardcoded-xmx-oomkilled.txt b/flags/docs/output/06-hardcoded-xmx-oomkilled.txt new file mode 100644 index 0000000..bc74302 --- /dev/null +++ b/flags/docs/output/06-hardcoded-xmx-oomkilled.txt @@ -0,0 +1,26 @@ +$ docker run --rm --memory=256m --entrypoint java flags-demo:jdk25 \ + -Xlog:gc -Xmx1g -cp /memoryhog.jar MemoryHog 24 200 +$ echo "exit code: $?" +$ docker inspect --format "OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}" + +[0.005s][info][gc] Using Serial +MemoryHog starting: chunk=24MB sleep=200ms +[0.054s][info][gc] GC(0) Pause Young (Allocation Failure) 1M->1M(7M) 0.685ms +held=24MB heapUsed=25MB heapMax=989MB freeHeap=6MB +[0.296s][info][gc] GC(1) Pause Young (Allocation Failure) 25M->25M(31M) 1.057ms +held=48MB heapUsed=49MB heapMax=989MB freeHeap=6MB +[0.513s][info][gc] GC(2) Pause Young (Allocation Failure) 49M->49M(77M) 0.606ms +held=72MB heapUsed=73MB heapMax=989MB freeHeap=27MB +[0.728s][info][gc] GC(3) Pause Young (Allocation Failure) 73M->73M(112M) 0.184ms +held=96MB heapUsed=98MB heapMax=989MB freeHeap=14MB +[0.959s][info][gc] GC(4) Pause Young (Allocation Failure) 98M->97M(147M) 15.593ms +held=120MB heapUsed=122MB heapMax=989MB freeHeap=24MB +[1.180s][info][gc] GC(5) Pause Young (Allocation Failure) 122M->121M(181M) 15.364ms +held=144MB heapUsed=146MB heapMax=989MB freeHeap=35MB +held=168MB heapUsed=170MB heapMax=989MB freeHeap=11MB +[1.634s][info][gc] GC(6) Pause Young (Allocation Failure) 170M->169M(251M) 34.003ms +held=192MB heapUsed=194MB heapMax=989MB freeHeap=56MB +held=216MB heapUsed=218MB heapMax=989MB freeHeap=32MB +container exit code: 137 + +error: no such object: hogB diff --git a/flags/docs/output/07-usecontainersupport-false-oomkilled.txt b/flags/docs/output/07-usecontainersupport-false-oomkilled.txt new file mode 100644 index 0000000..91b335b --- /dev/null +++ b/flags/docs/output/07-usecontainersupport-false-oomkilled.txt @@ -0,0 +1,31 @@ +$ docker run --rm --memory=256m --entrypoint java flags-demo:jdk25 \ + -XX:-UseContainerSupport -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|UseContainerSupport" +$ docker run --rm --memory=256m --entrypoint java flags-demo:jdk25 \ + -Xlog:gc -XX:-UseContainerSupport -cp /memoryhog.jar MemoryHog 24 200 + +--- what the flag alone resolves heap to --- + size_t MaxHeapSize = 2105540608 {product} {ergonomic} + size_t SoftMaxHeapSize = 2105540608 {manageable} {ergonomic} + bool UseContainerSupport = false {product} {command line} + +--- the hog run --- +[0.004s][info][gc] Using G1 +MemoryHog starting: chunk=24MB sleep=200ms +held=24MB heapUsed=26MB heapMax=2008MB freeHeap=101MB +held=48MB heapUsed=52MB heapMax=2008MB freeHeap=75MB +[0.515s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 52M->51M(128M) 1.445ms +[0.515s][info][gc] GC(1) Concurrent Undo Cycle +[0.515s][info][gc] GC(1) Concurrent Undo Cycle 0.084ms +held=72MB heapUsed=76MB heapMax=2008MB freeHeap=51MB +[0.731s][info][gc] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) 76M->76M(128M) 1.569ms +[0.732s][info][gc] GC(3) Concurrent Mark Cycle +[0.748s][info][gc] GC(3) Pause Remark 101M->101M(172M) 0.580ms +[0.749s][info][gc] GC(3) Pause Cleanup 101M->101M(172M) 0.018ms +held=96MB heapUsed=101MB heapMax=2008MB freeHeap=70MB +[0.751s][info][gc] GC(3) Concurrent Mark Cycle 19.274ms +held=120MB heapUsed=126MB heapMax=2008MB freeHeap=45MB +held=144MB heapUsed=151MB heapMax=2008MB freeHeap=45MB +held=168MB heapUsed=176MB heapMax=2008MB freeHeap=45MB +held=192MB heapUsed=201MB heapMax=2008MB freeHeap=45MB +held=216MB heapUsed=226MB heapMax=2008MB freeHeap=45MB +container exit code: 137 diff --git a/flags/docs/output/08-heapdumponoutofmemoryerror.txt b/flags/docs/output/08-heapdumponoutofmemoryerror.txt new file mode 100644 index 0000000..380512a --- /dev/null +++ b/flags/docs/output/08-heapdumponoutofmemoryerror.txt @@ -0,0 +1,23 @@ +$ docker run --rm --memory=256m -v $PWD/docker/memoryhog.jar:/memoryhog.jar --entrypoint java flags-demo:jdk25-jdk \ + -XX:MaxRAMPercentage=75 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps/heap.hprof \ + -cp /memoryhog.jar MemoryHog 24 200 + +=== Diagnostic: -XX:+HeapDumpOnOutOfMemoryError actually leaves a dump behind === +MemoryHog starting: chunk=24MB sleep=200ms +held=24MB heapUsed=25MB heapMax=185MB freeHeap=6MB +held=48MB heapUsed=49MB heapMax=185MB freeHeap=6MB +held=72MB heapUsed=73MB heapMax=185MB freeHeap=27MB +held=96MB heapUsed=98MB heapMax=185MB freeHeap=14MB +held=120MB heapUsed=122MB heapMax=185MB freeHeap=24MB +held=144MB heapUsed=146MB heapMax=185MB freeHeap=35MB +held=168MB heapUsed=170MB heapMax=185MB freeHeap=11MB +java.lang.OutOfMemoryError: Java heap space +Dumping heap to /dumps/heap.hprof ... +Heap dump file created [180390818 bytes in 1.028 secs] +Exception in thread "main" java.lang.OutOfMemoryError: Java heap space + at MemoryHog.main(MemoryHog.java:21) +exit=1 +total 176176 +drwxr-xr-x 2 root root 4096 Oct 1 04:29 . +drwxr-xr-x 3 root root 4096 Oct 1 04:29 .. +-rw------- 1 root root 180390818 Oct 1 04:29 heap.hprof diff --git a/flags/docs/output/09-native-memory-tracking.txt b/flags/docs/output/09-native-memory-tracking.txt new file mode 100644 index 0000000..26dd232 --- /dev/null +++ b/flags/docs/output/09-native-memory-tracking.txt @@ -0,0 +1,97 @@ +$ docker run -d --memory=512m --name nmtdemo -v $PWD/docker/flags-demo.jar:/app.jar --entrypoint java flags-demo:jdk25-jdk \ + -XX:NativeMemoryTracking=summary -XX:MaxRAMPercentage=75 -jar /app.jar +$ docker exec nmtdemo jcmd 1 VM.native_memory summary + +=== Diagnostic: -XX:NativeMemoryTracking=summary via jcmd === +1: + +Native Memory Tracking: + +(Omitting categories weighting less than 1KB) + +Total: reserved=1824154KB, committed=115878KB + malloc: 34018KB #144561, peak=35841KB #140626 + mmap: reserved=1790136KB, committed=81860KB + +- Java Heap (reserved=393216KB, committed=23532KB) + (mmap: reserved=393216KB, committed=23532KB, at peak) + +- Class (reserved=1049299KB, committed=5075KB) + (classes #8049) + ( instance classes #7466, array classes #583) + (malloc=723KB tag=Class #15577) (at peak) + (mmap: reserved=1048576KB, committed=4352KB, at peak) + ( Metadata: ) + ( reserved=65536KB, committed=27072KB) + ( used=26779KB) + ( waste=293KB =1.08%) + ( Class space:) + ( reserved=1048576KB, committed=4352KB) + ( used=4187KB) + ( waste=165KB =3.79%) + +- Thread (reserved=17468KB, committed=1380KB) + (threads #17) + (stack: reserved=17408KB, committed=1320KB, peak=1320KB) + (malloc=42KB tag=Thread #105) (peak=51KB #108) + (arena=18KB #30) (peak=301KB #22) + +- Code (reserved=252158KB, committed=16014KB) + (malloc=4465KB tag=Code #21734) (at peak) + (mmap: reserved=247692KB, committed=11548KB, at peak) + (arena=1KB #1) (peak=34KB #2) + +- GC (reserved=1302KB, committed=102KB) + (malloc=22KB tag=GC #58) (peak=96KB #79) + (mmap: reserved=1280KB, committed=80KB, at peak) + +- Compiler (reserved=8163KB, committed=8163KB) + (malloc=54KB tag=Compiler #86) (peak=69KB #91) + (arena=8110KB #14) (peak=22951KB #14) + +- Internal (reserved=1324KB, committed=1324KB) + (malloc=1288KB tag=Internal #5521) (at peak) + (mmap: reserved=36KB, committed=36KB, at peak) + +- Other (reserved=20KB, committed=20KB) + (malloc=20KB tag=Other #3) (at peak) + +- Symbol (reserved=6745KB, committed=6745KB) + (malloc=6034KB tag=Symbol #93254) (at peak) + (arena=712KB #1) (at peak) + +- Native Memory Tracking (reserved=2576KB, committed=2576KB) + (malloc=35KB tag=Native Memory Tracking #619) (at peak) + (tracking overhead=2541KB) + +- Shared class space (reserved=16384KB, committed=13912KB, readonly=0KB) + (mmap: reserved=16384KB, committed=13912KB, peak=14120KB) + +- Arena Chunk (reserved=9219KB, committed=9219KB) + (malloc=9219KB tag=Arena Chunk #541) (peak=23793KB #558) + +- Tracing (reserved=5KB, committed=5KB) + (malloc=5KB tag=Tracing #22) (at peak) + +- Module (reserved=80KB, committed=80KB) + (malloc=80KB tag=Module #2158) (at peak) + +- Safepoint (reserved=8KB, committed=8KB) + (mmap: reserved=8KB, committed=8KB, at peak) + +- Synchronization (reserved=483KB, committed=483KB) + (malloc=483KB tag=Synchronization #4738) (at peak) + +- Serviceability (reserved=17KB, committed=17KB) + (malloc=17KB tag=Serviceability #14) (peak=20KB #18) + +- Metaspace (reserved=65672KB, committed=27208KB) + (malloc=136KB tag=Metaspace #46) (at peak) + (mmap: reserved=65536KB, committed=27072KB, at peak) + +- String Deduplication (reserved=1KB, committed=1KB) + (malloc=1KB tag=String Deduplication #8) (at peak) + +- Object Monitors (reserved=13KB, committed=13KB) + (malloc=13KB tag=Object Monitors #69) (at peak) + diff --git a/flags/pom.xml b/flags/pom.xml new file mode 100644 index 0000000..88868dd --- /dev/null +++ b/flags/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.1.1 + + + + com.ankurm + flags-demo + 1.0.0 + jar + + + 25 + 25 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + + flags-demo + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/flags/scripts/run-all.sh b/flags/scripts/run-all.sh new file mode 100755 index 0000000..15f13c8 --- /dev/null +++ b/flags/scripts/run-all.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Regenerates every file under docs/output/. Requires Docker (a running daemon) +# and network access to pull eclipse-temurin:25-jre, eclipse-temurin:25-jdk, and +# ubuntu:24.04, plus ~280MB to download the Temurin 27 GA tarball (no official +# eclipse-temurin:27-* image exists on Docker Hub as of 2026-10-01, which is why +# docker/Dockerfile.jdk27 installs the tarball directly). +set -euo pipefail +cd "$(dirname "$0")/.." + +echo "== building images ==" +mvn -q -B clean package -DskipTests +cp target/flags-demo.jar docker/flags-demo.jar +if [ ! -f docker/temurin27.tar.gz ]; then + curl -sL -o docker/temurin27.tar.gz \ + "https://api.adoptium.net/v3/binary/latest/27/ga/linux/x64/jdk/hotspot/normal/eclipse" +fi +docker build -t flags-demo:jdk25 -f docker/Dockerfile.jdk25 docker +docker build -t flags-demo:jdk25-jdk -f docker/Dockerfile.jdk25-jdk docker +docker build -t flags-demo:jdk27 -f docker/Dockerfile.jdk27 docker + +echo "== 01/02: GC selection matrix, JDK 25 vs JDK 27 (JEP 523) ==" +combos=("256m:1" "512m:1" "1800m:1" "512m:2" "1800m:2" "4g:2") +for jdk in jdk25 jdk27; do + out="docs/output/0$([ "$jdk" = jdk25 ] && echo 1 || echo 2)-gc-selection-$jdk.txt" + : > "$out.tmp" + for c in "${combos[@]}"; do + mem="${c%%:*}"; cpu="${c##*:}" + echo "--- mem=$mem cpus=$cpu ---" >> "$out.tmp" + docker run --rm --memory="$mem" --cpus="$cpu" --entrypoint java "flags-demo:$jdk" \ + -XX:+PrintFlagsFinal -version 2>/dev/null \ + | grep -E "^\s*(bool UseG1GC|bool UseSerialGC|bool UseParallelGC|size_t MaxHeapSize|int ActiveProcessorCount|bool UseCompactObjectHeaders|double MaxRAMPercentage)\s*=" \ + >> "$out.tmp" + echo "" >> "$out.tmp" + done + mv "$out.tmp" "$out" +done + +echo "== 03: ActiveProcessorCount lying to the JVM ==" +{ + docker run --rm --cpus=1 --memory=1g --entrypoint java flags-demo:jdk27 \ + -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E "ParallelGCThreads|ConcGCThreads|ActiveProcessorCount" + echo "" + docker run --rm --cpus=1 --memory=1g --entrypoint java flags-demo:jdk27 \ + -XX:ActiveProcessorCount=8 -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E "ParallelGCThreads|ConcGCThreads|ActiveProcessorCount" +} > docs/output/03-activeprocessorcount-lie.txt.body + +echo "== 04: compact object headers, byte-for-byte (JEP 534) ==" +run_histo() { + local tag=$1 extra=$2 cname=$3 + docker run -d --memory=1g --name "$cname" -v "$PWD/docker/headersize.jar:/headersize.jar" \ + --entrypoint java "flags-demo:$tag" $extra -cp /headersize.jar HeaderSizeDemo 2000000 >/dev/null + sleep 6 + docker exec "$cname" jcmd 1 GC.class_histogram 2>&1 | grep -E "Pair|Total" + docker rm -f "$cname" >/dev/null 2>&1 +} +{ + echo "--- JDK 25, default (UseCompactObjectHeaders=false) ---" + run_histo jdk25-jdk "" hdr25default + echo "" + echo "--- JDK 25, -XX:+UseCompactObjectHeaders ---" + run_histo jdk25-jdk "-XX:+UseCompactObjectHeaders" hdr25on + echo "" + echo "--- JDK 27, default (UseCompactObjectHeaders=true) ---" + run_histo jdk27 "" hdr27default + echo "" + echo "--- JDK 27, -XX:-UseCompactObjectHeaders ---" + run_histo jdk27 "-XX:-UseCompactObjectHeaders" hdr27off +} > docs/output/04-compact-object-headers.txt.body + +echo "== 05: a correctly set MaxRAMPercentage fails safely ==" +docker run --rm --memory=256m -v "$PWD/docker/memoryhog.jar:/memoryhog.jar" --entrypoint java flags-demo:jdk25 \ + -Xlog:gc -XX:MaxRAMPercentage=75 -cp /memoryhog.jar MemoryHog 24 200 \ + > docs/output/05-maxrampercentage-correct-oom.txt.body 2>&1 || true + +echo "== 06: a hardcoded -Xmx bigger than the container gets the container killed ==" +docker run --rm --memory=256m -v "$PWD/docker/memoryhog.jar:/memoryhog.jar" --entrypoint java flags-demo:jdk25 \ + -Xlog:gc -Xmx1g -cp /memoryhog.jar MemoryHog 24 200 \ + > docs/output/06-hardcoded-xmx-oomkilled.txt.body 2>&1 +echo "(exit code above should be 137 - SIGKILL from the cgroup OOM killer)" + +echo "== 07: -XX:-UseContainerSupport makes the JVM blind to the container ==" +{ + docker run --rm --memory=256m --entrypoint java flags-demo:jdk25 \ + -XX:-UseContainerSupport -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E "MaxHeapSize|UseContainerSupport" + echo "" + docker run --rm --memory=256m -v "$PWD/docker/memoryhog.jar:/memoryhog.jar" --entrypoint java flags-demo:jdk25 \ + -Xlog:gc -XX:-UseContainerSupport -cp /memoryhog.jar MemoryHog 24 200 +} > docs/output/07-usecontainersupport-false-oomkilled.txt.body 2>&1 || true + +echo "== 08: -XX:+HeapDumpOnOutOfMemoryError actually leaves a dump ==" +mkdir -p /tmp/flags-demo-dumps +docker run --rm --memory=256m -v "$PWD/docker/memoryhog.jar:/memoryhog.jar" -v /tmp/flags-demo-dumps:/dumps \ + --entrypoint java flags-demo:jdk25-jdk \ + -XX:MaxRAMPercentage=75 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps/heap.hprof \ + -cp /memoryhog.jar MemoryHog 24 200 > docs/output/08-heapdumponoutofmemoryerror.txt.body 2>&1 || true +ls -la /tmp/flags-demo-dumps >> docs/output/08-heapdumponoutofmemoryerror.txt.body +rm -rf /tmp/flags-demo-dumps + +echo "== 09: -XX:NativeMemoryTracking=summary ==" +docker run -d --memory=512m --name nmtdemo -v "$PWD/docker/flags-demo.jar:/app.jar" --entrypoint java flags-demo:jdk25-jdk \ + -XX:NativeMemoryTracking=summary -XX:MaxRAMPercentage=75 -jar /app.jar >/dev/null +sleep 6 +docker exec nmtdemo jcmd 1 VM.native_memory summary > docs/output/09-native-memory-tracking.txt.body 2>&1 +docker rm -f nmtdemo >/dev/null 2>&1 + +echo "Done. Each docs/output/NN-*.txt.body is the fresh capture; prepend the command" +echo "comment block from the committed .txt file by hand if you are regenerating for a diff." diff --git a/flags/src/main/java/com/ankurm/flagsdemo/FlagsDemoApplication.java b/flags/src/main/java/com/ankurm/flagsdemo/FlagsDemoApplication.java new file mode 100644 index 0000000..56945cd --- /dev/null +++ b/flags/src/main/java/com/ankurm/flagsdemo/FlagsDemoApplication.java @@ -0,0 +1,19 @@ +package com.ankurm.flagsdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Companion app for "JVM Flags for Spring Boot in Containers (JDK 25/27)" on ankurm.com. + * See docs/output/*.txt for the captured runs this post quotes from, and + * {@link JvmDiagnosticsController} for the live /internal/jvm-flags endpoint + * used to inspect what the JVM actually resolved its memory/GC flags to + * inside a running container. Delete that endpoint before shipping this to + * a real service — it exists for this demo only. + */ +@SpringBootApplication +public class FlagsDemoApplication { + public static void main(String[] args) { + SpringApplication.run(FlagsDemoApplication.class, args); + } +} diff --git a/flags/src/main/java/com/ankurm/flagsdemo/JvmDiagnosticsController.java b/flags/src/main/java/com/ankurm/flagsdemo/JvmDiagnosticsController.java new file mode 100644 index 0000000..7d494c3 --- /dev/null +++ b/flags/src/main/java/com/ankurm/flagsdemo/JvmDiagnosticsController.java @@ -0,0 +1,83 @@ +package com.ankurm.flagsdemo; + +import com.sun.management.HotSpotDiagnosticMXBean; +import com.sun.management.OperatingSystemMXBean; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Prints what the JVM actually resolved its container-relevant flags to, at + * runtime, inside whatever cgroup it is running under. This is the live + * version of what docs/output/*.txt captures from -XX:+PrintFlagsFinal; the + * two should agree, and the post at ankurm.com cross-checks both. + * + *

Explained in the post: + * https://ankurm.com/jvm-flags-for-spring-boot-in-containers-jdk-25-27/ + * + *

Delete this endpoint before shipping to a real service. + * It leaks heap sizing and cgroup limits to anyone who can reach it. + */ +@RestController +public class JvmDiagnosticsController { + + private static final List FLAGS_OF_INTEREST = List.of( + "MaxRAMPercentage", "MinRAMPercentage", "InitialRAMPercentage", + "MaxRAM", "MaxHeapSize", "InitialHeapSize", "MinHeapSize", + "UseContainerSupport", "ActiveProcessorCount", + "UseG1GC", "UseParallelGC", "UseZGC", "UseSerialGC", + "UseCompactObjectHeaders", "ParallelGCThreads", "ConcGCThreads" + ); + + @GetMapping("/internal/jvm-flags") + public Map jvmFlags() { + Map out = new LinkedHashMap<>(); + + OperatingSystemMXBean os = + (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + Map hostView = new LinkedHashMap<>(); + hostView.put("availableProcessors", Runtime.getRuntime().availableProcessors()); + hostView.put("totalMemorySizeBytes", os.getTotalMemorySize()); + hostView.put("freeMemorySizeBytes", os.getFreeMemorySize()); + out.put("osMXBeanView", hostView); + + MemoryMXBean mem = ManagementFactory.getMemoryMXBean(); + MemoryUsage heap = mem.getHeapMemoryUsage(); + Map heapView = new LinkedHashMap<>(); + heapView.put("initBytes", heap.getInit()); + heapView.put("usedBytes", heap.getUsed()); + heapView.put("committedBytes", heap.getCommitted()); + heapView.put("maxBytes", heap.getMax()); + out.put("heap", heapView); + + List gcNames = ManagementFactory.getGarbageCollectorMXBeans() + .stream().map(GarbageCollectorMXBean::getName).toList(); + out.put("garbageCollectors", gcNames); + + HotSpotDiagnosticMXBean diag = + ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); + Map flagView = new LinkedHashMap<>(); + for (String name : FLAGS_OF_INTEREST) { + try { + flagView.put(name, diag.getVMOption(name).getValue()); + } catch (IllegalArgumentException notThisJvm) { + flagView.put(name, "N/A on this build"); + } + } + out.put("resolvedFlags", flagView); + + out.put("jvmInputArguments", + ManagementFactory.getRuntimeMXBean().getInputArguments()); + out.put("javaVersion", System.getProperty("java.version")); + out.put("javaVmName", System.getProperty("java.vm.name")); + + return out; + } +} diff --git a/flags/src/main/resources/application.yml b/flags/src/main/resources/application.yml new file mode 100644 index 0000000..c843cc5 --- /dev/null +++ b/flags/src/main/resources/application.yml @@ -0,0 +1,16 @@ +server: + port: 8080 + +management: + endpoints: + web: + exposure: + include: health,info + +spring: + application: + name: flags-demo + +logging: + level: + root: INFO diff --git a/flags/tools/HeaderSizeDemo.java b/flags/tools/HeaderSizeDemo.java new file mode 100644 index 0000000..e5a2d2c --- /dev/null +++ b/flags/tools/HeaderSizeDemo.java @@ -0,0 +1,26 @@ +import java.util.ArrayList; +import java.util.List; + +/** + * Allocates a large number of a trivial fixed-shape object and a trivial + * array, then sleeps so `jcmd GC.class_histogram` can be run against + * it from outside the container. Used to measure the per-instance byte cost + * that -XX:+/-UseCompactObjectHeaders actually makes, rather than quoting + * the JEP's own number. See + * https://ankurm.com/jvm-flags-for-spring-boot-in-containers-jdk-25-27/ + */ +public class HeaderSizeDemo { + static final class Pair { int a; int b; Pair(int a, int b) { this.a = a; this.b = b; } } + + public static void main(String[] args) throws Exception { + int count = args.length > 0 ? Integer.parseInt(args[0]) : 2_000_000; + List pairs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + pairs.add(new Pair(i, i)); + } + System.out.println("allocated " + pairs.size() + " Pair instances, pid=" + ProcessHandle.current().pid()); + System.gc(); + Thread.sleep(120_000); + System.out.println(pairs.size()); + } +} diff --git a/flags/tools/MemoryHog.java b/flags/tools/MemoryHog.java new file mode 100644 index 0000000..beed27b --- /dev/null +++ b/flags/tools/MemoryHog.java @@ -0,0 +1,37 @@ +import java.util.ArrayList; +import java.util.List; + +/** + * Deliberately allocates and retains heap in fixed-size chunks until it is + * killed or finishes, printing progress as it goes. Used to demonstrate what + * actually happens when a container's memory limit, the JVM's heap flags, + * and real allocation pressure disagree with each other. See the post at + * https://ankurm.com/jvm-flags-for-spring-boot-in-containers-jdk-25-27/ + * and docs/output/05-oom-*.txt / 06-oom-*.txt for the captured runs. + */ +public class MemoryHog { + public static void main(String[] args) throws Exception { + int chunkMb = args.length > 0 ? Integer.parseInt(args[0]) : 16; + int sleepMs = args.length > 1 ? Integer.parseInt(args[1]) : 150; + List held = new ArrayList<>(); + long totalMb = 0; + System.out.println("MemoryHog starting: chunk=" + chunkMb + "MB sleep=" + sleepMs + "ms"); + System.out.flush(); + while (true) { + byte[] chunk = new byte[chunkMb * 1024 * 1024]; + for (int i = 0; i < chunk.length; i += 4096) { + chunk[i] = 1; // touch each page so it is actually resident, not just reserved + } + held.add(chunk); + totalMb += chunkMb; + Runtime rt = Runtime.getRuntime(); + System.out.printf("held=%dMB heapUsed=%dMB heapMax=%dMB freeHeap=%dMB%n", + totalMb, + (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024), + rt.maxMemory() / (1024 * 1024), + rt.freeMemory() / (1024 * 1024)); + System.out.flush(); + Thread.sleep(sleepMs); + } + } +}