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,65 @@
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 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* Leak pattern 2 of 5: a ThreadLocal that outlives the request that created it.
*
* The fixed-size pool below is deliberately realistic: four threads, reused forever,
* exactly like a web server's request-handling pool. The bug is in {@link #handleOneRequest},
* which does what a lot of real "request context" code does: creates a *new* ThreadLocal
* instance per request to stash a payload, and never calls remove().
*
* A ThreadLocal's key is a WeakReference, so the ThreadLocal object itself can be collected.
* But Thread.ThreadLocalMap.Entry extends WeakReference<ThreadLocal&lt;?&gt;> and keeps a
* STRONG reference to the *value*. Expunging a stale entry (key==null) only happens on a later
* get()/set()/remove() call on that same slot -- which never comes, because every request makes
* a brand new ThreadLocal, so no one ever touches the old slot again. Each of the 4 pooled
* threads accumulates one dead-keyed, live-valued entry per request forever.
*/
@SpringBootApplication
public class ThreadLocalLeakApplication {
public static void main(String[] args) {
SpringApplication.run(ThreadLocalLeakApplication.class, args);
}
@Bean
ApplicationRunner driveLeak() {
return (ApplicationArguments args) -> {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(4);
pool.setMaxPoolSize(4);
pool.setQueueCapacity(Integer.MAX_VALUE);
pool.setThreadNamePrefix("req-worker-");
pool.initialize();
long payloadSize = 256 * 1024; // 256 KB "request context" per call
long i = 0;
System.out.println("threadlocal-leak: driving a 4-thread pool to OOM via leaked ThreadLocal entries");
while (true) {
final long reqId = i++;
pool.execute(() -> handleOneRequest(reqId, payloadSize));
if (reqId % 400 == 0) {
System.out.println("threadlocal-leak: submitted=" + reqId);
}
}
};
}
/**
* Every call creates a fresh ThreadLocal (the bug -- it should be a single static field)
* and never removes it. The whole point: this looks like ordinary per-request code.
*/
private static void handleOneRequest(long requestId, long payloadSize) {
ThreadLocal<byte[]> requestContext = new ThreadLocal<>();
requestContext.set(new byte[(int) payloadSize]);
requestContext.get()[0] = (byte) requestId; // "use" the context
// Missing on purpose: requestContext.remove();
}
}