113 lines
6.1 KiB
Markdown
113 lines
6.1 KiB
Markdown
# 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 <dump> 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
|
|
└── <module>/
|
|
├── 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).
|