spring-boot-demo-oom: five real memory-leak demos with captured OOM + MAT evidence
This commit is contained in:
@@ -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<?>> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user