From ff7b8ca842097dd0e4a5de0a6cde847ed910d85e Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 21 Nov 2019 16:16:15 +0100 Subject: [PATCH] make ClassLoaderLeakPreventorFactory configurable and mark BootstrapClassLoader as shutdown --- .../classloading/BootstrapClassLoader.java | 22 +++++ .../classloading/ClassLoaderLifeCycle.java | 97 ++++++++++++++++--- .../ClassLoaderLifeCycleTest.java | 7 +- 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/BootstrapClassLoader.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/BootstrapClassLoader.java index 64d9b75d36..a274dbb33c 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/BootstrapClassLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/BootstrapClassLoader.java @@ -5,7 +5,29 @@ package sonia.scm.lifecycle.classloading; * find it in a heap dump. */ class BootstrapClassLoader extends ClassLoader { + + /** + * Marker to find a BootstrapClassLoader, which is already shutdown. + */ + private boolean shutdown = false; + BootstrapClassLoader(ClassLoader webappClassLoader) { super(webappClassLoader); } + + /** + * Returns {@code true} if the classloader was shutdown. + * + * @return {@code true} if the classloader was shutdown + */ + boolean isShutdown() { + return shutdown; + } + + /** + * Mark the class loader as shutdown. + */ + void markAsShutdown() { + shutdown = true; + } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java index 24d1c239b4..a64ed6fa43 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java @@ -5,7 +5,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor; import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory; +import se.jiderhamn.classloader.leak.prevention.cleanup.IIOServiceProviderCleanUp; import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp; +import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp; +import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator; +import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator; import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator; import sonia.scm.lifecycle.LifeCycle; import sonia.scm.plugin.ChildFirstPluginClassLoader; @@ -16,9 +22,9 @@ import java.io.IOException; import java.net.URL; import java.util.ArrayDeque; import java.util.Deque; -import java.util.function.UnaryOperator; import static com.google.common.base.Preconditions.checkState; +import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT; /** * Creates and shutdown SCM-Manager ClassLoaders. @@ -27,23 +33,25 @@ public final class ClassLoaderLifeCycle implements LifeCycle { private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class); - private final Deque classLoaders = new ArrayDeque<>(); + private Deque classLoaders = new ArrayDeque<>(); private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory; private final ClassLoader webappClassLoader; - private ClassLoader bootstrapClassLoader; - private UnaryOperator classLoaderAppendListener = c -> c; + private BootstrapClassLoader bootstrapClassLoader; + + private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() { + @Override + public C apply(C classLoader) { + return classLoader; + } + }; @VisibleForTesting public static ClassLoaderLifeCycle create() { - ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(); - classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter()); - // the SunAwtAppContextInitiator causes a lot of exceptions and we use no awt - classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class); - // the MBeanCleanUp causes a Exception and we use no mbeans - classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class); - return new ClassLoaderLifeCycle(Thread.currentThread().getContextClassLoader(), classLoaderLeakPreventorFactory); + ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader(); + ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = createClassLoaderLeakPreventorFactory(webappClassLoader); + return new ClassLoaderLifeCycle(webappClassLoader, classLoaderLeakPreventorFactory); } ClassLoaderLifeCycle(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) { @@ -51,12 +59,64 @@ public final class ClassLoaderLifeCycle implements LifeCycle { this.webappClassLoader = initAndAppend(webappClassLoader); } + private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) { + // Should threads tied to the web app classloader be forced to stop at application shutdown? + boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads"); + + // Should Timer threads tied to the web app classloader be forced to stop at application shutdown? + boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads"); + + // Should shutdown hooks registered from the application be executed at application shutdown? + boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks"); + + // No of milliseconds to wait for threads to finish execution, before stopping them. + int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT); + + /* + * No of milliseconds to wait for shutdown hooks to finish execution, before stopping them. + * If set to -1 there will be no waiting at all, but Thread is allowed to run until finished. + */ + int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT); + + LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycle.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) ); + LOG.info(" stopThreads = {}", stopThreads); + LOG.info(" stopTimerThreads = {}", stopTimerThreads); + LOG.info(" executeShutdownHooks = {}", executeShutdownHooks); + LOG.info(" threadWaitMs = {} ms", threadWaitMs); + LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs); + + // use webapp classloader as safe base? or system? + ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader); + classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter()); + + final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class); + shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks); + shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs); + + final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class); + stopThreadsCleanUp.setStopThreads(stopThreads); + stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads); + stopThreadsCleanUp.setThreadWaitMs(threadWaitMs); + + // remove awt and imageio cleanup + classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class); + classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class); + classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class); + classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class); + classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class); + + // the MBeanCleanUp causes a Exception and we use no mbeans + classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class); + + return classLoaderLeakPreventorFactory; + } + public void initialize() { bootstrapClassLoader = initAndAppend(new BootstrapClassLoader(webappClassLoader)); } @VisibleForTesting - void setClassLoaderAppendListener(UnaryOperator classLoaderAppendListener) { + void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) { this.classLoaderAppendListener = classLoaderAppendListener; } @@ -84,12 +144,17 @@ public final class ClassLoaderLifeCycle implements LifeCycle { clap.shutdown(); clap = classLoaders.poll(); } + // be sure it is realy empty + classLoaders.clear(); + classLoaders = new ArrayDeque<>(); + + bootstrapClassLoader.markAsShutdown(); bootstrapClassLoader = null; } - private ClassLoader initAndAppend(ClassLoader originalClassLoader) { + private T initAndAppend(T originalClassLoader) { LOG.debug("init classloader {}", originalClassLoader); - ClassLoader classLoader = classLoaderAppendListener.apply(originalClassLoader); + T classLoader = classLoaderAppendListener.apply(originalClassLoader); ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader); preventor.runPreClassLoaderInitiators(); @@ -98,6 +163,10 @@ public final class ClassLoaderLifeCycle implements LifeCycle { return classLoader; } + interface ClassLoaderAppendListener { + C apply(C classLoader); + } + private class ClassLoaderAndPreventor { private final ClassLoader classLoader; diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java index 0ab98039d1..a8f37777d7 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java @@ -70,7 +70,12 @@ class ClassLoaderLifeCycleTest { URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(webappClassLoader); - lifeCycle.setClassLoaderAppendListener(c -> spy(c)); + lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycle.ClassLoaderAppendListener() { + @Override + public C apply(C classLoader) { + return spy(classLoader); + } + }); lifeCycle.initialize(); ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");