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
@@ -0,0 +1,15 @@
$ java -Xmx160m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=docs/output/heap.hprof -jar target/unbounded-queue-leak.jar
unbounded-queue-leak: driving an unbounded queue to OOM, consumer sleeps 50ms/item
java.lang.OutOfMemoryError: Java heap space
Dumping heap to docs/output/heap.hprof ...
Heap dump file created [175828534 bytes in 0.250 secs]
Caused by: java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.concurrent.LinkedBlockingQueue.put(LinkedBlockingQueue.java:329)
at com.ankurm.oomdemo.UnboundedQueueLeakApplication.lambda$driveLeak$0(UnboundedQueueLeakApplication.java:53)
at com.ankurm.oomdemo.UnboundedQueueLeakApplication$$Lambda/0x000000000b247688.run(Unknown Source)
at org.springframework.boot.SpringApplication.lambda$callRunner$0(SpringApplication.java:788)
at org.springframework.boot.SpringApplication$$Lambda/0x000000000b25ccd0.acceptWithException(Unknown Source)
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:82)
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60)
@@ -0,0 +1,23 @@
Eclipse MAT 1.17.0 batch report (org.eclipse.mat.api:suspects) on heap.hprof -- Problem Suspect 1
================================================================================================
Problem Suspect 1 One instance of java.util.concurrent.LinkedBlockingQueue loaded by <system
class loader> occupies 79,698,088 (92.41%) bytes. The top consumers of its minimum retained heap
are byte[] (152 instances totaling 79,694,208), java.util.concurrent.LinkedBlockingQueue$Node
(153 instances totaling 3,672), and java.util.concurrent.locks.ReentrantLock$NonfairSync (2
instances totaling 64). The instance is referenced by
com.ankurm.oomdemo.UnboundedQueueLeakApplication$$Lambda+0x000000000b25d428 @ 0xf63bb570 ,
loaded by org.springframework.boot.loader.launch.LaunchedClassLoader @ 0xf631bab8 . The memory
is accumulated in one instance of java.util.concurrent.LinkedBlockingQueue$Node , loaded by
<system class loader> , which occupies 1,572,984 (1.82%) bytes. Thread java.lang.Thread @
0xfcc19e38 main has a local variable or reference to java.util.concurrent.LinkedBlockingQueue @
0xff7386e8 which is on the shortest path to java.util.concurrent.LinkedBlockingQueue$Node @
0xfccce5c0 . The thread java.lang.Thread @ 0xfcc19e38 main keeps local variables with total size
632,832 (0.73%) bytes. The top consumers of its minimum retained heap are byte[] (152 instances
totaling 79,694,208), java.util.concurrent.LinkedBlockingQueue$Node (153 instances totaling
3,672), and java.util.concurrent.locks.ReentrantLock$NonfairSync (2 instances totaling 64).
Significant stack frames and local variables com.ankurm.oomdemo.UnboundedQueueLeakApplication.la
mbda$driveLeak$0(Lorg/springframework/boot/ApplicationArguments;)V
(UnboundedQueueLeakApplication.java:53) java.util.concurrent.LinkedBlockingQueue @ 0xff7386e8
retains 79,698,088 (92.41%) bytes The stacktrace of this Thread is available. See stacktrace .
See stacktrace with involved local variables .
+40
View File
@@ -0,0 +1,40 @@
<?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 http://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>unbounded-queue-leak</artifactId>
<version>1.0.0</version>
<name>unbounded-queue-leak</name>
<description>Leak pattern 5: a producer that outruns a consumer into an unbounded queue</description>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<finalName>unbounded-queue-leak</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Runs docs/output/heap.hprof (produced by scripts/run.sh) through Eclipse Memory Analyzer's
# headless batch report generator -- MemoryAnalyzer's -application org.eclipse.mat.api.parse,
# invoked via the ParseHeapDump.sh wrapper MAT ships -- then re-extracts the Problem Suspect
# summary into docs/output/02-mat-leak-suspects.txt.
#
# No GUI is shown, but MAT's SWT runtime still needs an X display even in this mode, hence
# xvfb-run. Needs: MAT_HOME pointing at an extracted Memory Analyzer 1.17.0+ "rcp" distribution,
# and xvfb-run on PATH (package "xvfb" on Debian/Ubuntu).
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT_DIR="$(cd "$MODULE_DIR/.." && pwd)"
cd "$MODULE_DIR"
: "${MAT_HOME:?Set MAT_HOME to your Memory Analyzer install (e.g. /opt/mat)}"
if [ ! -f docs/output/heap.hprof ]; then
echo "docs/output/heap.hprof not found -- run scripts/run.sh first" >&2
exit 1
fi
rm -f docs/output/heap_Leak_Suspects.zip
xvfb-run -a "$MAT_HOME/ParseHeapDump.sh" "$MODULE_DIR/docs/output/heap.hprof" org.eclipse.mat.api:suspects
python3 "$ROOT_DIR/scripts/extract-mat-summary.py" "$MODULE_DIR"
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Drives unbounded-queue-leak to a REAL java.lang.OutOfMemoryError under a constrained heap, with
# -XX:+HeapDumpOnOutOfMemoryError so the crash leaves a real .hprof behind. Also regenerates
# docs/output/01-oom-console.txt from the real run log.
#
# Needs JDK 25 (LTS) on PATH.
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT_DIR="$(cd "$MODULE_DIR/.." && pwd)"
cd "$MODULE_DIR"
mvn -q -B package -DskipTests
mkdir -p docs/output
rm -f docs/output/heap.hprof
CMD="java -Xmx160m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=docs/output/heap.hprof -jar target/unbounded-queue-leak.jar"
echo "== running: $CMD =="
LOG="$(mktemp)"
$CMD > "$LOG" 2>&1 || true
python3 "$ROOT_DIR/scripts/extract-oom-console.py" "$MODULE_DIR" "$LOG" "$CMD"
rm -f "$LOG"
echo "heap dump: docs/output/heap.hprof (not committed -- see .gitignore; regenerate it with this script)"
@@ -0,0 +1,61 @@
package com.ankurm.oomdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Leak pattern 5 of 5: a producer that outruns a consumer into a queue with no capacity bound.
*
* {@code new LinkedBlockingQueue<>()} with no capacity argument is unbounded -- it will accept
* elements until the heap is exhausted, full stop. That is the entire bug. It is also the most
* common of the five in real systems, because it usually arrives disguised as "a queue between
* a fast producer thread and a slow consumer thread," which sounds like ordinary
* producer/consumer code until the consumer falls behind (a slow downstream call, a paused
* consumer during a deploy, a poison message that makes every poll() take longer) and the queue
* becomes the de facto unbounded heap allocator for the whole payload stream.
*/
@SpringBootApplication
public class UnboundedQueueLeakApplication {
public static void main(String[] args) {
SpringApplication.run(UnboundedQueueLeakApplication.class, args);
}
@Bean
ApplicationRunner driveLeak() {
return (ApplicationArguments args) -> {
// The bug: no capacity bound. new LinkedBlockingQueue<>(10_000) would turn this
// whole failure mode into a put() that blocks instead of an OutOfMemoryError.
LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<>();
Thread consumer = new Thread(() -> {
while (true) {
try {
queue.take();
Thread.sleep(50); // the slow downstream call
} catch (InterruptedException e) {
return;
}
}
}, "slow-consumer");
consumer.setDaemon(true);
consumer.start();
long payloadSize = 512 * 1024; // 512 KB per message
long i = 0;
System.out.println("unbounded-queue-leak: driving an unbounded queue to OOM, consumer sleeps 50ms/item");
while (true) {
queue.put(new byte[(int) payloadSize]);
i++;
if (i % 200 == 0) {
System.out.println("unbounded-queue-leak: produced=" + i + " queueDepth=" + queue.size());
}
}
};
}
}