From e3433140a1e9a2f81018b60ebc9b6454a51a3030 Mon Sep 17 00:00:00 2001 From: markt Date: Tue, 1 Dec 2009 20:04:17 +0000 Subject: [PATCH] More memory leak protection. Adds support for: - optionally stopping threads started by a web app - this is dangerous last resort option for dev environments - not for production - clearing ThreadLocals created buy web apps - clearing unintentional references in sun.rmi.transport.Target git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@885901 13f79535-47bb-0310-9956-ffa450edef68 --- java/org/apache/catalina/core/StandardContext.java | 40 ++- .../apache/catalina/loader/LocalStrings.properties | 5 + .../apache/catalina/loader/WebappClassLoader.java | 317 +++++++++++++++++++-- java/org/apache/catalina/loader/WebappLoader.java | 3 +- webapps/docs/config/context.xml | 10 + 5 files changed, 351 insertions(+), 24 deletions(-) diff --git a/java/org/apache/catalina/core/StandardContext.java b/java/org/apache/catalina/core/StandardContext.java index 9b77f7d93..9bbf67646 100644 --- a/java/org/apache/catalina/core/StandardContext.java +++ b/java/org/apache/catalina/core/StandardContext.java @@ -739,7 +739,7 @@ public class StandardContext /** * Should Tomcat attempt to null out any static or final fields from loaded * classes when a web application is stopped as a work around for apparent - * garbage collection bugs and application coding errors. There have been + * garbage collection bugs and application coding errors? There have been * some issues reported with log4j when this option is true. Applications * without memory leaks using recent JVMs should operate correctly with this * option set to false. If not specified, the default value of @@ -747,6 +747,17 @@ public class StandardContext */ private boolean clearReferencesStatic = false; + /** + * Should Tomcat attempt to termiate threads that have been started by the + * web application? Stopping threads is performed via the deprecated (for + * good reason) Thread.stop() method and is likely to result in + * instability. As such, enabling this should be viewed as an option of last + * resort in a development environment and is not recommended in a + * production environment. If not specified, the default value of + * false will be used. + */ + private boolean clearReferencesStopThreads = false; + // ----------------------------------------------------- Context Properties @@ -2104,6 +2115,33 @@ public class StandardContext } + /** + * Return the clearReferencesStopThreads flag for this Context. + */ + public boolean getClearReferencesStopThreads() { + + return (this.clearReferencesStopThreads); + + } + + + /** + * Set the clearReferencesStatic feature for this Context. + * + * @param clearReferencesStatic The new flag value + */ + public void setClearReferencesStopThreads( + boolean clearReferencesStopThreads) { + + boolean oldClearReferencesStopThreads = this.clearReferencesStopThreads; + this.clearReferencesStopThreads = clearReferencesStopThreads; + support.firePropertyChange("clearReferencesStopThreads", + oldClearReferencesStopThreads, + this.clearReferencesStopThreads); + + } + + // -------------------------------------------------------- Context Methods diff --git a/java/org/apache/catalina/loader/LocalStrings.properties b/java/org/apache/catalina/loader/LocalStrings.properties index 43b843da5..d837e5aa0 100644 --- a/java/org/apache/catalina/loader/LocalStrings.properties +++ b/java/org/apache/catalina/loader/LocalStrings.properties @@ -34,6 +34,11 @@ webappClassLoader.jdbcRemoveStreamError=Exception closing input stream during JD webappClassLoader.stopped=Illegal access: this web application instance has been stopped already. Could not load {0}. The eventual following stack trace is caused by an error thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access, and has no functional impact. webappClassLoader.readError=Resource read error: Could not load {0}. webappClassLoader.clearJbdc=A web application registered the JBDC driver [{0}] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered. +webappClassLoader.clearRmiInfo=Failed to find class sun.rmi.transport.Target to clear context class loader. This is expected on non-Sun JVMs. +webappClassLoader.clearRmiFail=Failed to clear context class loader referenced from sun.rmi.transport.Target +webappClassLoader.clearThreadLocal=A web application created a ThreadLocal with key of type [{0}] (value [{1}]) and a value of type [{2}] (value [{3}]) but failed to remove it when the web application was stopped. To prevent a memory leak, the ThreadLocal has been forcibly removed. +webappClassLoader.clearThreadLocalFail=Failed to clear ThreadLocal references +webappClassLoader.stopThreadFail=Failed to terminate thread named [{0}] webappClassLoader.warnThread=A web application appears to have started a thread named [{0}] but has failed to stop it. This is very likely to create a memory leak. webappClassLoader.wrongVersion=(unable to load class {0}) webappLoader.addRepository=Adding repository {0} diff --git a/java/org/apache/catalina/loader/WebappClassLoader.java b/java/org/apache/catalina/loader/WebappClassLoader.java index 2b0f546ef..032a45223 100644 --- a/java/org/apache/catalina/loader/WebappClassLoader.java +++ b/java/org/apache/catalina/loader/WebappClassLoader.java @@ -24,7 +24,10 @@ import java.io.FileOutputStream; import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.Reference; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.MalformedURLException; import java.net.URL; @@ -45,6 +48,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Vector; +import java.util.concurrent.ThreadPoolExecutor; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -419,6 +423,17 @@ public class WebappClassLoader */ private boolean clearReferencesStatic = false; + /** + * Should Tomcat attempt to termiate threads that have been started by the + * web application? Stopping threads is performed via the deprecated (for + * goo reason) Thread.stop() method and is likely to result in + * instability. As such, enabling this should be viewed as an option of last + * resort in a development environment and is not recommended in a + * production environment. If not specified, the default value of + * false will be used. + */ + private boolean clearReferencesStopThreads = false; + // ------------------------------------------------------------- Properties @@ -590,6 +605,24 @@ public class WebappClassLoader } + /** + * Return the clearReferencesStatic flag for this Context. + */ + public boolean getClearReferencesStopThreads() { + return (this.clearReferencesStopThreads); + } + + + /** + * Set the clearReferencesStatic feature for this Context. + * + * @param clearReferencesStatic The new flag value + */ + public void setClearReferencesStopThreads( + boolean clearReferencesStopThreads) { + this.clearReferencesStopThreads = clearReferencesStopThreads; + } + // ------------------------------------------------------- Reloader Methods @@ -1678,6 +1711,12 @@ public class WebappClassLoader // Stop any threads the web application started clearReferencesThreads(); + // Clear any ThreadLocals loaded by this class loader + clearReferencesThreadLocals(); + + // Clear RMI Targets loaded by this class loader + clearReferencesRmiTargets(); + // Null out any static or final fields from loaded classes, // as a workaround for apparent garbage collection bugs if (clearReferencesStatic) { @@ -1883,30 +1922,15 @@ public class WebappClassLoader } + @SuppressWarnings("deprecation") private void clearReferencesThreads() { - // Get the current thread group - ThreadGroup tg = Thread.currentThread( ).getThreadGroup( ); - // Find the root thread group - while (tg.getParent() != null) { - tg = tg.getParent(); - } - - int threadCountGuess = tg.activeCount() + 50; - Thread[] threads = new Thread[threadCountGuess]; - int threadCountActual = tg.enumerate(threads); - // Make sure we don't miss any threads - while (threadCountActual == threadCountGuess) { - threadCountGuess *=2; - threads = new Thread[threadCountGuess]; - // Note tg.enumerate(Thread[]) silently ignores any threads that - // can't fit into the array - threadCountActual = tg.enumerate(threads); - } + Thread[] threads = getThreads(); // Iterate over the set of threads for (Thread thread : threads) { if (thread != null) { - if (thread.getContextClassLoader() == this) { + ClassLoader ccl = thread.getContextClassLoader(); + if (ccl != null && ccl == this) { // Don't warn about this thread if (thread == Thread.currentThread()) { continue; @@ -1918,21 +1942,270 @@ public class WebappClassLoader } // Don't warn about JVM controlled threads - if (thread.getThreadGroup() != null && - JVM_THREAD_GROUP_NAMES.contains( - thread.getThreadGroup().getName())) { + ThreadGroup tg = thread.getThreadGroup(); + if (tg != null && + JVM_THREAD_GROUP_NAMES.contains(tg.getName())) { continue; } log.error(sm.getString("webappClassLoader.warnThread", thread.getName())); + + // Don't try an stop the threads unless explicitly + // configured to do so + if (!clearReferencesStopThreads) { + continue; + } + + // If the thread has been started via an executor, try + // shutting down the executor + try { + Field targetField = + thread.getClass().getDeclaredField("target"); + targetField.setAccessible(true); + Object target = targetField.get(thread); + + if (target != null && + target.getClass().getCanonicalName().equals( + "java.util.concurrent.ThreadPoolExecutor.Worker")) { + Field executorField = + target.getClass().getDeclaredField("this$0"); + executorField.setAccessible(true); + Object executor = executorField.get(target); + if (executor instanceof ThreadPoolExecutor) { + ((ThreadPoolExecutor) executor).shutdownNow(); + } + } + } catch (SecurityException e) { + log.warn(sm.getString( + "webappClassLoader.stopThreadFail", + thread.getName()), e); + } catch (NoSuchFieldException e) { + log.warn(sm.getString( + "webappClassLoader.stopThreadFail", + thread.getName()), e); + } catch (IllegalArgumentException e) { + log.warn(sm.getString( + "webappClassLoader.stopThreadFail", + thread.getName()), e); + } catch (IllegalAccessException e) { + log.warn(sm.getString( + "webappClassLoader.stopThreadFail", + thread.getName()), e); + } + // This method is deprecated and for good reason. This is + // very risky code but is only only option at this point + // A *very* good reason for apps to do this clean-up + // themselves + thread.stop(); } } } } + + private void clearReferencesThreadLocals() { + Thread[] threads = getThreads(); + + try { + // Make the fields in the Thread class that store ThreadLocals + // accessible + Field threadLocalsField = + Thread.class.getDeclaredField("threadLocals"); + threadLocalsField.setAccessible(true); + Field inheritableThreadLocalsField = + Thread.class.getDeclaredField("inheritableThreadLocals"); + inheritableThreadLocalsField.setAccessible(true); + // Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects + // accessible + Class tlmClass = + Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); + Field tableField = tlmClass.getDeclaredField("table"); + tableField.setAccessible(true); + + for (int i = 0; i < threads.length; i++) { + Object threadLocalMap; + if (threads[i] != null) { + // Clear the first map + threadLocalMap = threadLocalsField.get(threads[i]); + clearThreadLocalMap(threadLocalMap, tableField); + // Clear the second map + threadLocalMap = + inheritableThreadLocalsField.get(threads[i]); + clearThreadLocalMap(threadLocalMap, tableField); + } + } + } catch (SecurityException e) { + log.warn(sm.getString("webappClassLoader.clearThreadLocalFail"), e); + } catch (NoSuchFieldException e) { + log.warn(sm.getString("webappClassLoader.clearThreadLocalFail"), e); + } catch (ClassNotFoundException e) { + log.warn(sm.getString("webappClassLoader.clearThreadLocalFail"), e); + } catch (IllegalArgumentException e) { + log.warn(sm.getString("webappClassLoader.clearThreadLocalFail"), e); + } catch (IllegalAccessException e) { + log.warn(sm.getString("webappClassLoader.clearThreadLocalFail"), e); + } catch (NoSuchMethodException e) { + log.warn(sm.getString("webappClassLoader.clearThreadLocalFail"), e); + } catch (InvocationTargetException e) { + log.warn(sm.getString("webappClassLoader.clearThreadLocalFail"), e); + } + } + + /* + * Clears the given thread local map object. Also pass in the field that + * points to the internal table to save re-calculating it on every + * call to this method. + */ + private void clearThreadLocalMap(Object map, Field internalTableField) + throws NoSuchMethodException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException { + if (map != null) { + Method mapRemove = + map.getClass().getDeclaredMethod("remove", + ThreadLocal.class); + mapRemove.setAccessible(true); + Object[] table = (Object[]) internalTableField.get(map); + if (table != null) { + for (int j =0; j < table.length; j++) { + if (table[j] != null) { + boolean remove = false; + // Check the key + Field keyField = + Reference.class.getDeclaredField("referent"); + keyField.setAccessible(true); + Object key = keyField.get(table[j]); + if (this.equals(key) || (key != null && + this == key.getClass().getClassLoader())) { + remove = true; + } + // Check the value + Field valueField = + table[j].getClass().getDeclaredField("value"); + valueField.setAccessible(true); + Object value = valueField.get(table[j]); + if (this.equals(value) || (value != null && + this == value.getClass().getClassLoader())) { + remove = true; + } + if (remove) { + Object entry = ((Reference) table[j]).get(); + Object[] args = new Object[4]; + if (key != null) { + args[0] = key.getClass().getCanonicalName(); + args[1] = key.toString(); + } + if (value != null) { + args[2] = value.getClass().getCanonicalName(); + args[3] = value.toString(); + } + log.error(sm.getString( + "webappClassLoader.clearThreadLocal", + args)); + mapRemove.invoke(map, entry); + } + } + } + } + } + } + + /* + * Get the set of current threads as an array. + */ + private Thread[] getThreads() { + // Get the current thread group + ThreadGroup tg = Thread.currentThread( ).getThreadGroup( ); + // Find the root thread group + while (tg.getParent() != null) { + tg = tg.getParent(); + } + + int threadCountGuess = tg.activeCount() + 50; + Thread[] threads = new Thread[threadCountGuess]; + int threadCountActual = tg.enumerate(threads); + // Make sure we don't miss any threads + while (threadCountActual == threadCountGuess) { + threadCountGuess *=2; + threads = new Thread[threadCountGuess]; + // Note tg.enumerate(Thread[]) silently ignores any threads that + // can't fit into the array + threadCountActual = tg.enumerate(threads); + } + + return threads; + } + + + /** + * This depends on the internals of the Sun JVM so it does everything by + * reflection. + */ + private void clearReferencesRmiTargets() { + try { + // Need access to the ccl field of sun.rmi.transport.Target + Class objectTargetClass = + Class.forName("sun.rmi.transport.Target"); + Field cclField = objectTargetClass.getDeclaredField("ccl"); + cclField.setAccessible(true); + + // Clear the objTable map + Class objectTableClass = + Class.forName("sun.rmi.transport.ObjectTable"); + Field objTableField = objectTableClass.getDeclaredField("objTable"); + objTableField.setAccessible(true); + Object objTable = objTableField.get(null); + if (objTable == null) { + return; + } + + // Iterate over the values in the table + if (objTable instanceof Map) { + Iterator iter = ((Map) objTable).values().iterator(); + while (iter.hasNext()) { + Object obj = iter.next(); + Object cclObject = cclField.get(obj); + if (this == cclObject) { + iter.remove(); + } + } + } + + // Clear the implTable map + Field implTableField = objectTableClass.getDeclaredField("implTable"); + implTableField.setAccessible(true); + Object implTable = implTableField.get(null); + if (implTable == null) { + return; + } + + // Iterate over the values in the table + if (implTable instanceof Map) { + Iterator iter = ((Map) implTable).values().iterator(); + while (iter.hasNext()) { + Object obj = iter.next(); + Object cclObject = cclField.get(obj); + if (this == cclObject) { + iter.remove(); + } + } + } + } catch (ClassNotFoundException e) { + log.info(sm.getString("webappClassLoader.clearRmiInfo"), e); + } catch (SecurityException e) { + log.warn(sm.getString("webappClassLoader.clearRmiFail"), e); + } catch (NoSuchFieldException e) { + log.warn(sm.getString("webappClassLoader.clearRmiFail"), e); + } catch (IllegalArgumentException e) { + log.warn(sm.getString("webappClassLoader.clearRmiFail"), e); + } catch (IllegalAccessException e) { + log.warn(sm.getString("webappClassLoader.clearRmiFail"), e); + } + } + + /** * Determine whether a class was loaded by this class loader or one of * its child class loaders. diff --git a/java/org/apache/catalina/loader/WebappLoader.java b/java/org/apache/catalina/loader/WebappLoader.java index 58367e90a..4c480027a 100644 --- a/java/org/apache/catalina/loader/WebappLoader.java +++ b/java/org/apache/catalina/loader/WebappLoader.java @@ -642,7 +642,8 @@ public class WebappLoader ((StandardContext) container).getAntiJARLocking()); classLoader.setClearReferencesStatic( ((StandardContext) container).getClearReferencesStatic()); - + classLoader.setClearReferencesStopThreads( + ((StandardContext) container).getClearReferencesStopThreads()); } for (int i = 0; i < repositories.length; i++) { diff --git a/webapps/docs/config/context.xml b/webapps/docs/config/context.xml index e79d8ad97..579cc4956 100644 --- a/webapps/docs/config/context.xml +++ b/webapps/docs/config/context.xml @@ -339,6 +339,16 @@ false will be used.

+ +

If true, Tomcat attempts to termiate threads that have + been started by the web application? Stopping threads is performed via + the deprecated (for good reason) Thread.stop() method and + is likely to result in instability. As such, enabling this should be + viewed as an option of last resort in a development environment and is + not recommended in a production environment.If not specified, the + default value of false will be used.

+
+

Whether the context should process TLDs on startup. The default is true. The false setting is intended for special cases -- 2.11.0