Files

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();
}
}
}