78 lines
3.4 KiB
Java
78 lines
3.4 KiB
Java
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.io.ByteArrayOutputStream;
|
|
import java.io.InputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Leak pattern 4 of 5: a hot-reloaded plugin whose old classloader is never released.
|
|
*
|
|
* {@code PluginTask.class} (compiled from plugin-src/, see scripts/build-plugin.sh) is loaded
|
|
* fresh on every iteration through a brand-new {@link PluginClassLoader} -- exactly what a
|
|
* plugin system, a rules-engine hot-swap, or a servlet container's redeploy does. That part is
|
|
* normal and is not the bug.
|
|
*
|
|
* The bug is LIVE_PLUGINS: every reloaded instance is appended and never removed. Because the
|
|
* instance's {@code getClass()} points at the Class object defined by *that* reload's
|
|
* PluginClassLoader, keeping the instance alive keeps its Class alive, which keeps its
|
|
* ClassLoader alive, which keeps every other class that loader ever defined alive too. A real
|
|
* version of this bug is usually one line: a static registry, metrics map, or "active plugins"
|
|
* list that an unload path forgets to clear.
|
|
*/
|
|
@SpringBootApplication
|
|
public class ClassloaderLeakApplication {
|
|
|
|
/** The leak: every reload's instance (and therefore its whole classloader) kept forever. */
|
|
static final List<Runnable> LIVE_PLUGINS = new ArrayList<>();
|
|
|
|
public static void main(String[] args) {
|
|
SpringApplication.run(ClassloaderLeakApplication.class, args);
|
|
}
|
|
|
|
@Bean
|
|
ApplicationRunner driveLeak() {
|
|
return (ApplicationArguments args) -> {
|
|
byte[] classBytes = readPluginClassBytes();
|
|
String className = "com.ankurm.oomplugin.PluginTask";
|
|
long i = 0;
|
|
System.out.println("classloader-leak: driving repeated plugin reloads to OOM, class=" + className);
|
|
while (true) {
|
|
PluginClassLoader loader = new PluginClassLoader(
|
|
ClassloaderLeakApplication.class.getClassLoader(), className, classBytes);
|
|
Class<?> pluginClass = loader.loadClass(className);
|
|
Runnable plugin = (Runnable) pluginClass.getDeclaredConstructor().newInstance();
|
|
plugin.run();
|
|
|
|
// The bug: this reload's plugin instance, Class, and ClassLoader are never
|
|
// forgotten. A correct version would reload into a new list and let the old
|
|
// list (and everything it reaches) become unreachable.
|
|
LIVE_PLUGINS.add(plugin);
|
|
|
|
i++;
|
|
if (i % 150 == 0) {
|
|
System.out.println("classloader-leak: reloads=" + i + " livePlugins=" + LIVE_PLUGINS.size());
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private static byte[] readPluginClassBytes() throws Exception {
|
|
try (InputStream in = ClassloaderLeakApplication.class.getClassLoader()
|
|
.getResourceAsStream("plugin/PluginTask.class")) {
|
|
if (in == null) {
|
|
throw new IllegalStateException("plugin/PluginTask.class not found on the classpath");
|
|
}
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
in.transferTo(out);
|
|
return out.toByteArray();
|
|
}
|
|
}
|
|
}
|