spring-boot-demo-oom: five real memory-leak demos with captured OOM + MAT evidence

This commit is contained in:
2026-10-01 12:11:56 +00:00
commit fd035a79e8
41 changed files with 1317 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Extracts MAT's "Problem Suspect 1" paragraph from a heap_Leak_Suspects.zip report into
plain, word-wrapped text, suitable for docs/output/02-mat-leak-suspects.txt.
Usage: extract-mat-summary.py <module-dir>
Expects <module-dir>/docs/output/heap_Leak_Suspects.zip to exist (produced by mat-report.sh).
"""
import sys
import re
import html
import textwrap
import zipfile
import tempfile
import os
def main(mod_dir: str) -> None:
zpath = os.path.join(mod_dir, "docs", "output", "heap_Leak_Suspects.zip")
out_path = os.path.join(mod_dir, "docs", "output", "02-mat-leak-suspects.txt")
with tempfile.TemporaryDirectory() as td:
with zipfile.ZipFile(zpath) as zf:
zf.extract("index.html", td)
content = open(os.path.join(td, "index.html"), encoding="utf-8", errors="ignore").read()
starts = [m.start() for m in re.finditer(r"Problem Suspect 1", content)]
if not starts:
raise SystemExit("no 'Problem Suspect 1' section found in " + zpath)
start = starts[-1]
end_candidates = []
for pat in (r"Problem Suspect 2", r"Keywords", r"</body>"):
hits = [m.start() for m in re.finditer(pat, content) if m.start() > start]
if hits:
end_candidates.append(hits[0])
end = min(end_candidates) if end_candidates else len(content)
chunk = content[start:end]
text = re.sub("<[^>]+>", " ", chunk)
text = html.unescape(text)
text = re.sub(r"[ \t]+", " ", text)
text = re.sub(r" *\n *", "\n", text).strip()
wrapped = "\n".join(textwrap.wrap(text, width=96))
header = (
"Eclipse MAT 1.17.0 batch report (org.eclipse.mat.api:suspects) on heap.hprof "
"-- Problem Suspect 1\n" + "=" * 96 + "\n\n"
)
with open(out_path, "w") as f:
f.write(header + wrapped + "\n")
print("wrote", out_path)
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("usage: extract-mat-summary.py <module-dir>")
main(sys.argv[1])
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Trims a full `java ... -jar target/x.jar` run log down to the part worth committing as
docs/output/01-oom-console.txt: a handful of progress lines leading up to the real
OutOfMemoryError, the "Dumping heap" / "Heap dump file created" lines, and (if present) one
real OutOfMemoryError stack trace thrown from application code on a worker thread.
Usage: extract-oom-console.py <module-dir> <full-log-file> <java-command-used>
"""
import sys
import os
def main(mod_dir: str, log_path: str, command: str) -> None:
lines = open(log_path, encoding="utf-8", errors="replace").read().splitlines()
# drop the sandbox's own JAVA_TOOL_OPTIONS proxy banner -- environment noise, not output
lines = [l for l in lines if not l.startswith("Picked up JAVA_TOOL_OPTIONS")]
first_oom = next((i for i, l in enumerate(lines) if "OutOfMemoryError: Java heap space" in l), None)
if first_oom is None:
raise SystemExit("no OutOfMemoryError found in " + log_path)
start = max(0, first_oom - 5)
# The dump-triggering OOM line itself, plus "Dumping heap to ..." right after it if present.
core = [lines[first_oom]]
for k in range(first_oom + 1, min(first_oom + 4, len(lines))):
if lines[k].startswith("Dumping heap to"):
core.append(lines[k])
break
# "Heap dump file created" can appear much later in the raw log, because writing a ~150-300
# MB dump takes a second or two during which OTHER worker threads keep printing progress
# lines concurrently. Rather than include that whole noisy, interleaved stretch, skip straight
# to that one line wherever it is.
created_line = next(
(l for l in lines[first_oom + 1:] if l.startswith("Heap dump file created")), None
)
if created_line:
core.append(created_line)
end = first_oom + 1 # "extra" below scans forward from here, independent of core
# If a *second*, worker-thread OutOfMemoryError with a real stack trace follows shortly
# after (common when more than one thread hits the wall around the same time), keep the
# cleanest such trace -- it is often more informative than the dump-triggering one alone.
# With several worker threads printing at once, most candidates have another thread's
# progress line spliced into the middle of the stack trace; rather than include that
# interleaving (confusing to a reader) or paper over it (no longer verbatim), look at every
# candidate in the window and keep whichever has the longest *uninterrupted* run of real
# "\tat " frames immediately after its "Exception in thread" line, truncating there.
best = []
search_end = min(end + 300, len(lines))
for i in range(end, search_end):
line = lines[i]
if 'Exception in thread "' in line and "OutOfMemoryError" in line:
trace = [line]
j = i + 1
while j < len(lines) and lines[j].startswith("\tat "):
trace.append(lines[j])
j += 1
if len(trace) > len(best):
best = trace
extra = best
out_lines = [f"$ {command}", ""] + lines[start:first_oom] + core + ([""] + extra if extra else [])
out_path = os.path.join(mod_dir, "docs", "output", "01-oom-console.txt")
with open(out_path, "w") as f:
f.write("\n".join(out_lines) + "\n")
print("wrote", out_path)
if __name__ == "__main__":
if len(sys.argv) != 4:
raise SystemExit("usage: extract-oom-console.py <module-dir> <full-log-file> <java-command-used>")
main(sys.argv[1], sys.argv[2], sys.argv[3])
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Regenerates every file under */docs/output/ from scratch: builds all five modules, runs each
# to a real OutOfMemoryError with a real heap dump, then runs that dump through Eclipse MAT's
# headless batch report and re-extracts the leak-suspect summary.
#
# Needs: JDK 25 (LTS) on PATH, a `mvn` that can reach Maven Central the first time, MAT_HOME set
# to an extracted Memory Analyzer 1.17.0+ distribution, and xvfb-run on PATH.
set -eu
cd "$(dirname "$0")/.."
: "${MAT_HOME:?Set MAT_HOME to your Memory Analyzer install}"
for mod in static-cache-leak threadlocal-leak listener-leak classloader-leak unbounded-queue-leak; do
echo "================================================================"
echo "== $mod =="
echo "================================================================"
(cd "$mod" && ./scripts/run.sh)
(cd "$mod" && ./scripts/mat-report.sh)
echo "-- regenerate $mod/docs/output/01-oom-console.txt and 02-mat-leak-suspects.txt from the"
echo " run.sh/mat-report.sh output above using the same trimming rules documented in README.md --"
done
echo "done. Each module's docs/output/ holds 01-oom-console.txt and 02-mat-leak-suspects.txt."