# spring-boot-demo-oom Five small Spring Boot apps, each leaking memory a different way. Every one is driven to a **real** `java.lang.OutOfMemoryError` under a constrained heap, with a real `.hprof` heap dump captured at the moment it happens, then run through Eclipse Memory Analyzer's headless batch report generator for real leak-suspect evidence. Nothing here is a description of what a leak "would" look like -- every number and stack trace under `*/docs/output/` comes from a file a script produced. Companion repository for [Debugging OutOfMemoryError: Heap Dumps, Eclipse MAT, and the Five Leak Patterns in Spring Apps](https://ankurm.com/debugging-outofmemoryerror-heap-dumps-eclipse-mat-and-the-five-leak-patterns-in-spring-apps/) on ankurm.com. ### Versions | Component | Version | Verified against | |---|---|---| | JDK | **25.0.4.1+1** (Temurin, LTS) | `java -version` | | Spring Boot | **4.1.1** | `repo1.maven.org/maven2/.../spring-boot-starter-parent/maven-metadata.xml` (latest non-milestone GA at write time) | | Eclipse Memory Analyzer | **1.17.0** (2026-06-01 RCP build) | `ftp-stud.hs-esslingen.de/pub/Mirrors/eclipse/mat/1.17.0/rcp/` | | Maven | 3.9.11 | `mvn -version` | ### The five leaks | Module | Leak pattern | What actually retains the memory | |---|---|---| | [`static-cache-leak`](static-cache-leak/) | Static cache, no eviction | `ConcurrentHashMap` on a `static` field, grown by one entry per unique key forever | | [`threadlocal-leak`](threadlocal-leak/) | `ThreadLocal` on a pooled executor | A *new* `ThreadLocal` created per task, value never `remove()`d, on 4 reused pool threads | | [`listener-leak`](listener-leak/) | Observer never unregistered | A singleton listener list only ever grows; each listener closes over a session's buffer | | [`classloader-leak`](classloader-leak/) | Hot-reloaded plugin, classloader never released | A registry of past "reload" instances keeps every one's `Class` + `ClassLoader` alive | | [`unbounded-queue-leak`](unbounded-queue-leak/) | Producer outruns consumer into an unbounded queue | `new LinkedBlockingQueue<>()` with no capacity bound, between a fast producer and a 50ms/item consumer | Each module is a standalone Spring Boot app (`spring-boot-starter`, no web layer -- nothing here needs HTTP to make its point) whose `ApplicationRunner` drives the leak in a tight loop until the JVM runs out of heap. ### Quickstart ```bash export JAVA_HOME=/path/to/jdk-25 # must be JDK 25 or newer export PATH="$JAVA_HOME/bin:$PATH" cd static-cache-leak ./scripts/run.sh # builds, runs with -Xmx160m, crashes with a real OutOfMemoryError, # regenerates docs/output/01-oom-console.txt ``` `scripts/run.sh` always rebuilds and rewrites `docs/output/01-oom-console.txt` from that run's real log. The heap dump itself (`docs/output/heap.hprof`, 140-300 MB per module) is **not** committed -- see `.gitignore` -- regenerate it with the same script. ### Running it through Eclipse MAT ```bash export MAT_HOME=/path/to/MemoryAnalyzer-1.17.0...-linux.gtk.x86_64/ # extracted RCP build cd static-cache-leak ./scripts/mat-report.sh # needs xvfb-run -- MAT's SWT runtime wants an X display even # in this headless report mode, so xvfb-run -a wraps it ``` This calls MAT's `ParseHeapDump.sh org.eclipse.mat.api:suspects` -- the command-line batch report generator MAT ships alongside its GUI, no interactive session required -- and then re-extracts the "Problem Suspect 1" paragraph into `docs/output/02-mat-leak-suspects.txt`. Regenerate every module's output in one go with `scripts/run-all.sh` from the repo root (needs `MAT_HOME` set). ### Index of captured output | File | What it proves | |---|---| | `*/docs/output/01-oom-console.txt` | The real `java -Xmx...m -XX:+HeapDumpOnOutOfMemoryError` run: progress lines, the real `OutOfMemoryError`, the real "Heap dump file created" line, and (where available) a clean worker-thread stack trace into the leaking call | | `*/docs/output/02-mat-leak-suspects.txt` | Eclipse MAT's own "Problem Suspect 1" paragraph from the real `.hprof`: the class/instance holding the memory, its retained-size percentage, the top consumer classes, and the thread/path that reaches it | All ten files are regenerated from scratch, per module, by `scripts/run.sh` + `scripts/mat-report.sh` -- see `scripts/extract-oom-console.py` and `scripts/extract-mat-summary.py` for exactly how each is trimmed out of the raw tool output (both scripts are plain, auditable Python -- no hand-retyping of either tool's text at any point). ### Why no web layer Every demo is a `CommandLineRunner`-style `ApplicationRunner` bean that starts leaking the moment the context comes up. A real HTTP-triggered leak looks the same under MAT once you have a heap dump -- the point of this repository is the dump-and-analyze workflow and the five retention shapes, not building five web services. ### Source layout ``` spring-boot-demo-oom/ ├── pom.xml aggregator -- lists the five modules, no shared parent ├── scripts/ │ ├── run-all.sh regenerates every module's docs/output/ (needs MAT_HOME) │ ├── extract-oom-console.py trims a raw run log into 01-oom-console.txt │ └── extract-mat-summary.py trims a raw MAT report zip into 02-mat-leak-suspects.txt └── / ├── pom.xml parent: org.springframework.boot:spring-boot-starter-parent:4.1.1 ├── scripts/ │ ├── run.sh build + run to a real OOM + regenerate 01-oom-console.txt │ └── mat-report.sh run the resulting .hprof through MAT + regenerate 02-*.txt ├── src/main/java/com/ankurm/oomdemo/... └── docs/output/*.txt captured real output (see index above) ``` `classloader-leak/` additionally has `plugin-src/` (the hot-reloaded plugin's source, compiled by `classloader-leak/scripts/build-plugin.sh` into a committed `.class` resource -- see that module's README section in the post for why it has to be a resource rather than an ordinary compiled class). ## License MIT -- see [LICENSE](LICENSE).