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]>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
target/
|
||||
docker/temurin27.tar.gz
|
||||
docker/flags-demo.jar
|
||||
docker/memoryhog.jar
|
||||
docker/headersize.jar
|
||||
*.hprof
|
||||
*.class
|
||||
raw/
|
||||
+111
@@ -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`.
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
@@ -0,0 +1,27 @@
|
||||
$ docker run -d --memory=1g --name hdr -v $PWD/docker/headersize.jar:/headersize.jar \
|
||||
--entrypoint java flags-demo:<tag> [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 ([email protected])
|
||||
Total 2030279 57368512
|
||||
|
||||
--- JDK 25, -XX:+UseCompactObjectHeaders ---
|
||||
1: 2000000 32000000 HeaderSizeDemo$Pair
|
||||
300: 1 16 java.lang.invoke.StringConcatFactory$InlineHiddenClassStrategy$MethodHandlePair ([email protected])
|
||||
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.
|
||||
@@ -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
|
||||
@@ -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 <id> --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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>flags-demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
<maven.compiler.release>25</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>flags-demo</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
Executable
+107
@@ -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."
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Explained in the post:
|
||||
* https://ankurm.com/jvm-flags-for-spring-boot-in-containers-jdk-25-27/
|
||||
*
|
||||
* <p><strong>Delete this endpoint before shipping to a real service.</strong>
|
||||
* It leaks heap sizing and cgroup limits to anyone who can reach it.
|
||||
*/
|
||||
@RestController
|
||||
public class JvmDiagnosticsController {
|
||||
|
||||
private static final List<String> 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<String, Object> jvmFlags() {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
|
||||
OperatingSystemMXBean os =
|
||||
(OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
Map<String, Object> 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<String, Object> 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<String> gcNames = ManagementFactory.getGarbageCollectorMXBeans()
|
||||
.stream().map(GarbageCollectorMXBean::getName).toList();
|
||||
out.put("garbageCollectors", gcNames);
|
||||
|
||||
HotSpotDiagnosticMXBean diag =
|
||||
ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
Map<String, String> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: flags-demo
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
@@ -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 <pid> 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<Pair> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<byte[]> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user