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