ASF Bugzilla – Attachment 26150 Details for
Bug 49159
Improve ThreadLocal memory leak clean-up
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
patch tc7 renew threads RC1
patch tc7 renewThreads 2010-10-09.txt (text/plain), 42.32 KB, created by
Sylvain Laurent
on 2010-10-09 17:14:35 UTC
(
hide
)
Description:
patch tc7 renew threads RC1
Filename:
MIME Type:
Creator:
Sylvain Laurent
Created:
2010-10-09 17:14:35 UTC
Size:
42.32 KB
patch
obsolete
>Index: conf/server.xml >=================================================================== >--- conf/server.xml (revision 1006037) >+++ conf/server.xml (working copy) >@@ -28,6 +28,7 @@ > <!-- Prevent memory leaks due to use of particular java/javax APIs--> > <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> > <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> >+ <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" /> > > <!-- Global JNDI resources > Documentation at /docs/jndi-resources-howto.html >Index: java/org/apache/catalina/core/StandardContext.java >=================================================================== >--- java/org/apache/catalina/core/StandardContext.java (revision 1006037) >+++ java/org/apache/catalina/core/StandardContext.java (working copy) >@@ -775,15 +775,14 @@ > * default value of <code>false</code> will be used. > */ > private boolean clearReferencesStopTimerThreads = false; >- >+ > /** >- * Should Tomcat attempt to clear any ThreadLocal objects that are instances >- * of classes loaded by this class loader. Failure to remove any such >- * objects will result in a memory leak on web application stop, undeploy or >- * reload. It is disabled by default since the clearing of the ThreadLocal >- * objects is not performed in a thread-safe manner. >+ * Should Tomcat renew the threads of the thread pool when the application >+ * is stopped to avoid memory leaks because of uncleaned ThreadLocal >+ * variables. This also requires that the threadRenewalDelay property of the >+ * StandardThreadExecutor of ThreadPoolExecutor be set to a positive value. > */ >- private boolean clearReferencesThreadLocals = false; >+ private boolean renewThreadsWhenStoppingContext = true; > > /** > * Should the effective web.xml be logged when the context starts? >@@ -2351,30 +2350,22 @@ > } > > >- /** >- * Return the clearReferencesThreadLocals flag for this Context. >- */ >- public boolean getClearReferencesThreadLocals() { >+ public boolean getRenewThreadsWhenStoppingContext() { >+ return this.renewThreadsWhenStoppingContext; >+ } > >- return (this.clearReferencesThreadLocals); >- >+ public void setRenewThreadsWhenStoppingContext(boolean renewThreadsWhenStoppingContext) { >+ boolean oldRenewThreadsWhenStoppingContext = >+ this.renewThreadsWhenStoppingContext; >+ this.renewThreadsWhenStoppingContext = renewThreadsWhenStoppingContext; >+ support.firePropertyChange("renewThreadsWhenStoppingContext", >+ oldRenewThreadsWhenStoppingContext, >+ this.renewThreadsWhenStoppingContext); > } > >- >- /** >- * Set the clearReferencesThreadLocals feature for this Context. >- * >- * @param clearReferencesThreadLocals The new flag value >- */ > public void setClearReferencesThreadLocals( > boolean clearReferencesThreadLocals) { > >- boolean oldClearReferencesThreadLocals = >- this.clearReferencesThreadLocals; >- this.clearReferencesThreadLocals = clearReferencesThreadLocals; >- support.firePropertyChange("clearReferencesStopThreads", >- oldClearReferencesThreadLocals, >- this.clearReferencesThreadLocals); > > } > >Index: java/org/apache/catalina/core/StandardThreadExecutor.java >=================================================================== >--- java/org/apache/catalina/core/StandardThreadExecutor.java (revision 1006037) >+++ java/org/apache/catalina/core/StandardThreadExecutor.java (working copy) >@@ -84,6 +84,13 @@ > */ > protected int maxQueueSize = Integer.MAX_VALUE; > >+ /** >+ * After a context is stopped, threads in the pool are renewed. To avoid >+ * renewing all threads at the same time, this delay is observed between 2 >+ * threads being renewed. >+ */ >+ protected long threadRenewalDelay = 1000L; >+ > private TaskQueue taskqueue = null; > // ---------------------------------------------- Constructors > public StandardThreadExecutor() { >@@ -165,6 +172,12 @@ > } > } else throw new IllegalStateException("StandardThreadPool not started."); > } >+ >+ public void contextStopping() { >+ if (executor != null) { >+ executor.contextStopping(); >+ } >+ } > > public int getThreadPriority() { > return threadPriority; >@@ -250,6 +263,18 @@ > return maxQueueSize; > } > >+ public long getThreadRenewalDelay() { >+ return threadRenewalDelay; >+ } >+ >+ public void setThreadRenewalDelay(long threadRenewalDelay) { >+ this.threadRenewalDelay = threadRenewalDelay; >+ if(executor!=null) { >+ executor.setThreadRenewalDelay(threadRenewalDelay); >+ } >+ } >+ >+ > // Statistics from the thread pool > @Override > public int getActiveCount() { >Index: java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java >=================================================================== >--- java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java (revision 0) >+++ java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java (revision 0) >@@ -0,0 +1,209 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You under the Apache License, Version 2.0 >+ * (the "License"); you may not use this file except in compliance with >+ * the License. You may obtain a copy of the License at >+ * >+ * http://www.apache.org/licenses/LICENSE-2.0 >+ * >+ * Unless required by applicable law or agreed to in writing, software >+ * distributed under the License is distributed on an "AS IS" BASIS, >+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. >+ * See the License for the specific language governing permissions and >+ * limitations under the License. >+ */ >+ >+package org.apache.catalina.core; >+ >+import java.util.concurrent.Executor; >+ >+import org.apache.catalina.Container; >+import org.apache.catalina.ContainerEvent; >+import org.apache.catalina.ContainerListener; >+import org.apache.catalina.Context; >+import org.apache.catalina.Engine; >+import org.apache.catalina.Host; >+import org.apache.catalina.Lifecycle; >+import org.apache.catalina.LifecycleEvent; >+import org.apache.catalina.LifecycleListener; >+import org.apache.catalina.Server; >+import org.apache.catalina.Service; >+import org.apache.catalina.connector.Connector; >+import org.apache.coyote.ProtocolHandler; >+import org.apache.coyote.ajp.AjpAprProtocol; >+import org.apache.coyote.ajp.AjpProtocol; >+import org.apache.coyote.http11.AbstractHttp11Protocol; >+import org.apache.coyote.http11.Http11AprProtocol; >+import org.apache.juli.logging.Log; >+import org.apache.juli.logging.LogFactory; >+import org.apache.tomcat.util.threads.ThreadPoolExecutor; >+ >+/** >+ * A {@link LifecycleListener} that triggers the renewal of threads in Executor >+ * pools when a {@link Context} is being stopped to avoid thread-local related >+ * memory leaks.<br/> >+ * Note : active threads will be renewed one by one when they come back to the >+ * pool after executing their task, see >+ * {@link org.apache.tomcat.util.threads.ThreadPoolExecutor}.afterExecute().<br/> >+ * >+ * This listener must be declared in server.xml to be active. >+ * >+ * @author slaurent >+ * >+ */ >+public class ThreadLocalLeakPreventionListener implements LifecycleListener, ContainerListener { >+ private static final Log log = LogFactory.getLog(ThreadLocalLeakPreventionListener.class); >+ >+ /** >+ * Listens for {@link LifecycleEvent} for the start of the {@link Server} to >+ * initialize itself and then for after_stop events of each {@link Context}. >+ */ >+ @Override >+ public void lifecycleEvent(LifecycleEvent event) { >+ try { >+ Lifecycle lifecycle = event.getLifecycle(); >+ if (Lifecycle.AFTER_START_EVENT.equals(event.getType()) && lifecycle instanceof Server) { >+ // when the server starts, we register ourself as listener for >+ // all context >+ // as well as container event listener so that we know when new >+ // Context are deployed >+ Server server = (Server) lifecycle; >+ registerListenersForServer(server); >+ } >+ >+ if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType()) && lifecycle instanceof Context) { >+ stopIdleThreads((Context) lifecycle); >+ } >+ } catch (Exception e) { >+ log.error("Exception processing event " + event, e); >+ } >+ } >+ >+ @Override >+ public void containerEvent(ContainerEvent event) { >+ try { >+ String type = event.getType(); >+ if (Container.ADD_CHILD_EVENT.equals(type)) { >+ processContainerAddChild(event.getContainer(), (Container) event.getData()); >+ } else if (Container.REMOVE_CHILD_EVENT.equals(type)) { >+ processContainerRemoveChild(event.getContainer(), (Container) event.getData()); >+ } >+ } catch (Exception e) { >+ log.error("Exception processing event " + event, e); >+ } >+ >+ } >+ >+ private void registerListenersForServer(Server server) { >+ for (Service service : server.findServices()) { >+ Engine engine = (Engine) service.getContainer(); >+ engine.addContainerListener(this); >+ registerListenersForEngine(engine); >+ } >+ >+ } >+ >+ private void registerListenersForEngine(Engine engine) { >+ for (Container hostContainer : engine.findChildren()) { >+ Host host = (Host) hostContainer; >+ host.addContainerListener(this); >+ registerListenersForHost(host); >+ } >+ } >+ >+ private void registerListenersForHost(Host host) { >+ for (Container contextContainer : host.findChildren()) { >+ Context context = (Context) contextContainer; >+ registerContextListener(context); >+ } >+ } >+ >+ private void registerContextListener(Context context) { >+ context.addLifecycleListener(this); >+ } >+ >+ protected void processContainerAddChild(Container parent, Container child) { >+ if (log.isDebugEnabled()) >+ log.debug("Process addChild[parent=" + parent + ",child=" + child + "]"); >+ >+ try { >+ if (child instanceof Context) { >+ registerContextListener((Context) child); >+ } else if (child instanceof Engine) { >+ registerListenersForEngine((Engine) child); >+ } else if (child instanceof Host) { >+ registerListenersForHost((Host) child); >+ } >+ } catch (Throwable t) { >+ log.error("processContainerAddChild: Throwable", t); >+ } >+ >+ } >+ >+ protected void processContainerRemoveChild(Container parent, Container child) { >+ >+ if (log.isDebugEnabled()) >+ log.debug("Process removeChild[parent=" + parent + ",child=" + child + "]"); >+ >+ try { >+ if (child instanceof Context) { >+ Context context = (Context) child; >+ context.removeLifecycleListener(this); >+ } else if (child instanceof Host) { >+ Host host = (Host) child; >+ host.removeContainerListener(this); >+ } else if (child instanceof Engine) { >+ Engine engine = (Engine) child; >+ engine.removeContainerListener(this); >+ } >+ } catch (Throwable t) { >+ log.error("processContainerRemoveChild: Throwable", t); >+ } >+ >+ } >+ >+ /** >+ * Updates each ThreadPoolExecutor with the current time, which is the time >+ * when a context is being stopped. >+ * >+ * @param context >+ * the context being stopped, used to discover all the Connectors >+ * of its parent Service. >+ */ >+ private void stopIdleThreads(Context context) { >+ if (context instanceof StandardContext && !((StandardContext) context).getRenewThreadsWhenStoppingContext()) { >+ log.debug("Not renewing threads when the context is stopping, it is configured not to do it."); >+ return; >+ } >+ >+ Engine engine = (Engine) context.getParent().getParent(); >+ Service service = engine.getService(); >+ Connector[] connectors = service.findConnectors(); >+ if (connectors != null) { >+ for (Connector connector : connectors) { >+ ProtocolHandler handler = connector.getProtocolHandler(); >+ Executor executor = null; >+ if (handler instanceof AbstractHttp11Protocol) { >+ executor = ((AbstractHttp11Protocol) handler).getExecutor(); >+ } else if (handler instanceof AjpProtocol) { >+ executor = ((AjpProtocol) handler).getExecutor(); >+ } else if (handler instanceof AjpAprProtocol) { >+ executor = ((AjpAprProtocol) handler).getExecutor(); >+ } else if (handler instanceof Http11AprProtocol) { >+ executor = ((Http11AprProtocol) handler).getExecutor(); >+ } >+ >+ if (executor instanceof ThreadPoolExecutor) { >+ ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; >+ threadPoolExecutor.contextStopping(); >+ } else if (executor instanceof StandardThreadExecutor) { >+ StandardThreadExecutor stdThreadExecutor = (StandardThreadExecutor) executor; >+ stdThreadExecutor.contextStopping(); >+ } >+ >+ } >+ } >+ } >+} >Index: java/org/apache/catalina/core/mbeans-descriptors.xml >=================================================================== >--- java/org/apache/catalina/core/mbeans-descriptors.xml (revision 1006037) >+++ java/org/apache/catalina/core/mbeans-descriptors.xml (working copy) >@@ -115,10 +115,6 @@ > description="Should Tomcat attempt to terminate threads that have been started by the web application? Advisable to be used only in a development environment." > type="boolean"/> > >- <attribute name="clearReferencesThreadLocals" >- description="Should Tomcat attempt to clear any ThreadLocal objects that are instances of classes loaded by this class loader. " >- type="boolean"/> >- > <attribute name="clearReferencesStopTimerThreads" > description="Should Tomcat attempt to terminate TimerThreads that have been started by the web application? Advisable to be used only in a development environment." > type="boolean"/> >@@ -272,6 +268,10 @@ > description="The reloadable flag for this web application" > type="boolean"/> > >+ <attribute name="renewThreadsWhenStoppingContext" >+ description="Should Tomcat renew the threads of the thread pool when the application is stopped to avoid memory leaks because of uncleaned ThreadLocal variables." >+ type="boolean"/> >+ > <attribute name="saveConfig" > description="Should the configuration be written as needed on startup" > is="true" >@@ -1521,6 +1521,11 @@ > <attribute name="threadPriority" > description="The thread priority for threads in this thread pool" > type="int"/> >+ >+ <attribute name="threadRenewalDelay" >+ description="After a context is stopped, threads in the pool are renewed. To avoid renewing all threads at the same time, this delay is observed between 2 threads being renewed. Value is in ms, default value is 1000ms. If negative, threads are not renewed." >+ type="long"/> >+ > </mbean> > > <mbean name="StandardWrapper" >Index: java/org/apache/catalina/loader/LocalStrings.properties >=================================================================== >--- java/org/apache/catalina/loader/LocalStrings.properties (revision 1006037) >+++ java/org/apache/catalina/loader/LocalStrings.properties (working copy) >@@ -44,11 +44,9 @@ > webappClassLoader.clearReferencesResourceBundlesFail=Failed to clear ResourceBundle references for web application [{0}] > webappClassLoader.clearRmiInfo=Failed to find class sun.rmi.transport.Target to clear context class loader for web application [{0}]. This is expected on non-Sun JVMs. > webappClassLoader.clearRmiFail=Failed to clear context class loader referenced from sun.rmi.transport.Target for web application [{0}] >-webappClassLoader.clearThreadLocalDebug=The web application [{0}] created a ThreadLocal with key of type [{1}] (value [{2}]). The ThreadLocal has been correctly set to null and the key will be removed by GC. >-webappClassLoader.clearThreadLocal=The web application [{0}] created a ThreadLocal with key of type [{1}] (value [{2}]) and a value of type [{3}] (value [{4}]) but failed to remove it when the web application was stopped. This is very likely to create a memory leak. >-webappClassLoader.clearThreadLocalDebugClear=To simplify the process of tracing memory leaks, the key has been forcibly removed. >-webappClassLoader.clearThreadLocalClear=To prevent a memory leak, the ThreadLocal has been forcibly removed. >-webappClassLoader.clearThreadLocalFail=Failed to clear ThreadLocal references for web application [{0}] >+webappClassLoader.checkThreadLocalsForLeaksDebug=The web application [{0}] created a ThreadLocal with key of type [{1}] (value [{2}]). The ThreadLocal has been correctly set to null and the key will be removed by GC. >+webappClassLoader.checkThreadLocalsForLeaks=The web application [{0}] created a ThreadLocal with key of type [{1}] (value [{2}]) and a value of type [{3}] (value [{4}]) but failed to remove it when the web application was stopped. This is very likely to create a memory leak, but threads are going to be renewed over time so that the leak should be mitigated. >+webappClassLoader.checkThreadLocalsForLeaksFail=Failed to check for ThreadLocal references for web application [{0}] > webappClassLoader.stopThreadFail=Failed to terminate thread named [{0}] for web application [{1}] > webappClassLoader.stopTimerThreadFail=Failed to terminate TimerThread named [{0}] for web application [{1}] > webappClassLoader.validationErrorJarPath=Unable to validate JAR entry with name {0} >Index: java/org/apache/catalina/loader/WebappClassLoader.java >=================================================================== >--- java/org/apache/catalina/loader/WebappClassLoader.java (revision 1006037) >+++ java/org/apache/catalina/loader/WebappClassLoader.java (working copy) >@@ -459,15 +459,6 @@ > private boolean clearReferencesStopTimerThreads = false; > > /** >- * Should Tomcat attempt to clear any ThreadLocal objects that are instances >- * of classes loaded by this class loader. Failure to remove any such >- * objects will result in a memory leak on web application stop, undeploy or >- * reload. It is disabled by default since the clearing of the ThreadLocal >- * objects is not performed in a thread-safe manner. >- */ >- private boolean clearReferencesThreadLocals = false; >- >- /** > * Should Tomcat call {@link org.apache.juli.logging.LogFactory#release()} > * when the class loader is stopped? If not specified, the default value > * of <code>true</code> is used. Changing the default setting is likely to >@@ -755,25 +746,6 @@ > } > > >- /** >- * Return the clearReferencesThreadLocals flag for this Context. >- */ >- public boolean getClearReferencesThreadLocals() { >- return (this.clearReferencesThreadLocals); >- } >- >- >- /** >- * Set the clearReferencesThreadLocals feature for this Context. >- * >- * @param clearReferencesThreadLocals The new flag value >- */ >- public void setClearReferencesThreadLocals( >- boolean clearReferencesThreadLocals) { >- this.clearReferencesThreadLocals = clearReferencesThreadLocals; >- } >- >- > // ------------------------------------------------------- Reloader Methods > > >@@ -1951,7 +1923,7 @@ > clearReferencesThreads(); > > // Clear any ThreadLocals loaded by this class loader >- clearReferencesThreadLocals(); >+ checkThreadLocalsForLeaks(); > > // Clear RMI Targets loaded by this class loader > clearReferencesRmiTargets(); >@@ -2348,7 +2320,7 @@ > } > } > >- private void clearReferencesThreadLocals() { >+ private void checkThreadLocalsForLeaks() { > Thread[] threads = getThreads(); > > try { >@@ -2372,73 +2344,66 @@ > if (threads[i] != null) { > // Clear the first map > threadLocalMap = threadLocalsField.get(threads[i]); >- clearThreadLocalMap(threadLocalMap, tableField); >+ checkThreadLocalMapForLeaks(threadLocalMap, tableField); > // Clear the second map > threadLocalMap = > inheritableThreadLocalsField.get(threads[i]); >- clearThreadLocalMap(threadLocalMap, tableField); >+ checkThreadLocalMapForLeaks(threadLocalMap, tableField); > } > } > } catch (SecurityException e) { >- log.warn(sm.getString("webappClassLoader.clearThreadLocalFail", >+ log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", > contextName), e); > } catch (NoSuchFieldException e) { >- log.warn(sm.getString("webappClassLoader.clearThreadLocalFail", >+ log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", > contextName), e); > } catch (ClassNotFoundException e) { >- log.warn(sm.getString("webappClassLoader.clearThreadLocalFail", >+ log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", > contextName), e); > } catch (IllegalArgumentException e) { >- log.warn(sm.getString("webappClassLoader.clearThreadLocalFail", >+ log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", > contextName), e); > } catch (IllegalAccessException e) { >- log.warn(sm.getString("webappClassLoader.clearThreadLocalFail", >+ log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", > contextName), e); > } catch (NoSuchMethodException e) { >- log.warn(sm.getString("webappClassLoader.clearThreadLocalFail", >+ log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", > contextName), e); > } catch (InvocationTargetException e) { >- log.warn(sm.getString("webappClassLoader.clearThreadLocalFail", >+ log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", > contextName), e); > } > } > > > /* >- * Clears the given thread local map object. Also pass in the field that >+ * Analyzes 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) >+ private void checkThreadLocalMapForLeaks(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); >- int staleEntriesCount = 0; > if (table != null) { > for (int j =0; j < table.length; j++) { > if (table[j] != null) { >- boolean remove = false; >+ boolean potentialLeak = false; > // Check the key > Object key = ((Reference<?>) table[j]).get(); >- if (this.equals(key) || (key != null && >- this == key.getClass().getClassLoader())) { >- remove = true; >+ if (this.equals(key) || objectIsLoadedByThisOrChildClassLoader(key)) { >+ potentialLeak = 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 (this.equals(value) || objectIsLoadedByThisOrChildClassLoader(value)) { >+ potentialLeak = true; > } >- if (remove) { >+ if (potentialLeak) { > Object[] args = new Object[5]; > args[0] = contextName; > if (key != null) { >@@ -2448,43 +2413,20 @@ > if (value != null) { > args[3] = value.getClass().getCanonicalName(); > args[4] = value.toString(); >- } >- if (value == null) { >+ log.error(sm.getString( >+ "webappClassLoader.checkThreadLocalsForLeaks", >+ args)); >+ } else { > if (log.isDebugEnabled()) { > log.debug(sm.getString( >- "webappClassLoader.clearThreadLocalDebug", >+ "webappClassLoader.checkThreadLocalsForLeaksDebug", > args)); >- if (clearReferencesThreadLocals) { >- log.debug(sm.getString( >- "webappClassLoader.clearThreadLocalDebugClear")); >- } > } >- } else { >- log.error(sm.getString( >- "webappClassLoader.clearThreadLocal", >- args)); >- if (clearReferencesThreadLocals) { >- log.info(sm.getString( >- "webappClassLoader.clearThreadLocalClear")); >- } > } >- if (clearReferencesThreadLocals) { >- if (key == null) { >- staleEntriesCount++; >- } else { >- mapRemove.invoke(map, key); >- } >- } > } > } > } > } >- if (staleEntriesCount > 0) { >- Method mapRemoveStale = >- map.getClass().getDeclaredMethod("expungeStaleEntries"); >- mapRemoveStale.setAccessible(true); >- mapRemoveStale.invoke(map); >- } > } > } > >@@ -2674,6 +2616,23 @@ > } > > >+ private boolean objectIsLoadedByThisOrChildClassLoader(Object o) { >+ if(o == null) { >+ return false; >+ } >+ >+ Class<?> clazz = o.getClass(); >+ if(o instanceof Class) { >+ clazz = (Class<?>)clazz; >+ } >+ >+ for(ClassLoader cl = clazz.getClassLoader(); cl != null; cl = cl.getParent()) { >+ if(cl == this) { >+ return true; >+ } >+ } >+ return false; >+ } > /** > * Determine whether a class was loaded by this class loader or one of > * its child class loaders. >Index: java/org/apache/catalina/loader/WebappLoader.java >=================================================================== >--- java/org/apache/catalina/loader/WebappLoader.java (revision 1006037) >+++ java/org/apache/catalina/loader/WebappLoader.java (working copy) >@@ -592,8 +592,6 @@ > ((StandardContext) container).getClearReferencesStopThreads()); > classLoader.setClearReferencesStopTimerThreads( > ((StandardContext) container).getClearReferencesStopTimerThreads()); >- classLoader.setClearReferencesThreadLocals( >- ((StandardContext) container).getClearReferencesThreadLocals()); > } > > for (int i = 0; i < repositories.length; i++) { >Index: java/org/apache/tomcat/util/threads/TaskQueue.java >=================================================================== >--- java/org/apache/tomcat/util/threads/TaskQueue.java (revision 1006037) >+++ java/org/apache/tomcat/util/threads/TaskQueue.java (working copy) >@@ -34,6 +34,10 @@ > private static final long serialVersionUID = 1L; > > private ThreadPoolExecutor parent = null; >+ >+ // no need to be volatile, the one times when we change and read it occur in >+ // a single thread (the one that did stop a context and fired listeners) >+ private Integer forcedRemainingCapacity = null; > > public TaskQueue() { > super(); >@@ -74,4 +78,44 @@ > //if we reached here, we need to add it to the queue > return super.offer(o); > } >+ >+ >+ @Override >+ public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { >+ Runnable runnable = super.poll(timeout, unit); >+ if (runnable == null && parent != null) { >+ // the poll timed out, it gives an opportunity to stop the current >+ // thread if needed to avoid memory leaks. >+ parent.stopCurrentThreadIfNeeded(); >+ } >+ return runnable; >+ } >+ >+ >+ @Override >+ public Runnable take() throws InterruptedException { >+ if (parent != null && parent.currentThreadShouldBeStopped()) { >+ return poll(parent.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); >+ //yes, this may return null (in case of timeout) which normally does not occur with take() >+ //but the ThreadPoolExecutor implementation allows this >+ } >+ return super.take(); >+ } >+ >+ @Override >+ public int remainingCapacity() { >+ if(forcedRemainingCapacity != null) { >+ // ThreadPoolExecutor.setCorePoolSize checks that >+ // remainingCapacity==0 to allow to interrupt idle threads >+ // I don't see why, but this hack allows to conform to this >+ // "requirement" >+ return forcedRemainingCapacity.intValue(); >+ } >+ return super.remainingCapacity(); >+ } >+ >+ public void setForcedRemainingCapacity(Integer forcedRemainingCapacity) { >+ this.forcedRemainingCapacity = forcedRemainingCapacity; >+ } >+ > } >Index: java/org/apache/tomcat/util/threads/TaskThread.java >=================================================================== >--- java/org/apache/tomcat/util/threads/TaskThread.java (revision 0) >+++ java/org/apache/tomcat/util/threads/TaskThread.java (revision 0) >@@ -0,0 +1,47 @@ >+/* >+ * Licensed to the Apache Software Foundation (ASF) under one or more >+ * contributor license agreements. See the NOTICE file distributed with >+ * this work for additional information regarding copyright ownership. >+ * The ASF licenses this file to You under the Apache License, Version 2.0 >+ * (the "License"); you may not use this file except in compliance with >+ * the License. You may obtain a copy of the License at >+ * >+ * http://www.apache.org/licenses/LICENSE-2.0 >+ * >+ * Unless required by applicable law or agreed to in writing, software >+ * distributed under the License is distributed on an "AS IS" BASIS, >+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. >+ * See the License for the specific language governing permissions and >+ * limitations under the License. >+ */ >+package org.apache.tomcat.util.threads; >+ >+/** >+ * A Thread implementation that records the time at which it was created. >+ * >+ * @author slaurent >+ * >+ */ >+public class TaskThread extends Thread { >+ >+ private final long creationTime; >+ >+ public TaskThread(ThreadGroup group, Runnable target, String name) { >+ super(group, target, name); >+ this.creationTime = System.currentTimeMillis(); >+ } >+ >+ public TaskThread(ThreadGroup group, Runnable target, String name, >+ long stackSize) { >+ super(group, target, name, stackSize); >+ this.creationTime = System.currentTimeMillis(); >+ } >+ >+ /** >+ * @return the time (in ms) at which this thread was created >+ */ >+ public final long getCreationTime() { >+ return creationTime; >+ } >+ >+} >Index: java/org/apache/tomcat/util/threads/TaskThreadFactory.java >=================================================================== >--- java/org/apache/tomcat/util/threads/TaskThreadFactory.java (revision 1006037) >+++ java/org/apache/tomcat/util/threads/TaskThreadFactory.java (working copy) >@@ -39,7 +39,7 @@ > > @Override > public Thread newThread(Runnable r) { >- Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement()); >+ TaskThread t = new TaskThread(group, r, namePrefix + threadNumber.getAndIncrement()); > t.setDaemon(daemon); > t.setPriority(threadPriority); > return t; >Index: java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java >=================================================================== >--- java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java (revision 1006037) >+++ java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java (working copy) >@@ -16,12 +16,18 @@ > */ > package org.apache.tomcat.util.threads; > >+import java.lang.Thread.UncaughtExceptionHandler; > import java.util.concurrent.BlockingQueue; > import java.util.concurrent.RejectedExecutionException; > import java.util.concurrent.RejectedExecutionHandler; > import java.util.concurrent.ThreadFactory; > import java.util.concurrent.TimeUnit; > import java.util.concurrent.atomic.AtomicInteger; >+import java.util.concurrent.atomic.AtomicLong; >+ >+import org.apache.juli.logging.Log; >+import org.apache.juli.logging.LogFactory; >+ > /** > * Same as a java.util.concurrent.ThreadPoolExecutor but implements a much more efficient > * {@link #getSubmittedCount()} method, to be used to properly handle the work queue. >@@ -31,7 +37,8 @@ > * > */ > public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { >- >+ private static final Log log = LogFactory.getLog(ThreadPoolExecutor.class); >+ > /** > * The number of tasks submitted but not yet finished. This includes tasks > * in the queue and tasks that have been handed to a worker thread but the >@@ -39,7 +46,20 @@ > * This number is always greater or equal to {@link #getActiveCount()}. > */ > private final AtomicInteger submittedCount = new AtomicInteger(0); >- >+ private final AtomicLong lastContextStoppedTime = new AtomicLong(0L); >+ >+ /** >+ * Most recent time in ms when a thread decided to kill itself to avoid >+ * potential memory leaks. Useful to throttle the rate of renewals of >+ * threads. >+ */ >+ private final AtomicLong lastTimeThreadKilledItself = new AtomicLong(0L); >+ >+ /** >+ * Delay in ms between 2 threads being renewed. If negative, do not renew threads. >+ */ >+ private long threadRenewalDelay = 1000L; >+ > public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { > super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); > } >@@ -56,12 +76,60 @@ > public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { > super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new RejectHandler()); > } >+ >+ public long getThreadRenewalDelay() { >+ return threadRenewalDelay; >+ } > >+ public void setThreadRenewalDelay(long threadRenewalDelay) { >+ this.threadRenewalDelay = threadRenewalDelay; >+ } >+ > @Override > protected void afterExecute(Runnable r, Throwable t) { > submittedCount.decrementAndGet(); >+ >+ if (t == null) { >+ stopCurrentThreadIfNeeded(); >+ } > } > >+ /** >+ * If the current thread was started before the last time when a context was >+ * stopped, an exception is thrown so that the current thread is stopped. >+ */ >+ protected void stopCurrentThreadIfNeeded() { >+ if (currentThreadShouldBeStopped()) { >+ long lastTime = lastTimeThreadKilledItself.longValue(); >+ if (lastTime + threadRenewalDelay < System.currentTimeMillis()) { >+ if (lastTimeThreadKilledItself.compareAndSet(lastTime, System.currentTimeMillis() + 1)) { >+ // OK, it's really time to dispose of this thread >+ >+ final String msg = "Stopping thread " + Thread.currentThread().getName() >+ + " to avoid potential memory leaks after a context was stopped."; >+ Thread.currentThread().setUncaughtExceptionHandler(new UncaughtExceptionHandler() { >+ @Override >+ public void uncaughtException(Thread t, Throwable e) { >+ // yes, swallow the exception >+ log.debug(msg); >+ } >+ }); >+ throw new RuntimeException(msg); >+ } >+ } >+ } >+ } >+ >+ protected boolean currentThreadShouldBeStopped() { >+ if (threadRenewalDelay >= 0 && Thread.currentThread() instanceof TaskThread) { >+ TaskThread currentTaskThread = (TaskThread) Thread.currentThread(); >+ if (currentTaskThread.getCreationTime() < this.lastContextStoppedTime.longValue()) { >+ return true; >+ } >+ } >+ return false; >+ } >+ > public int getSubmittedCount() { > return submittedCount.get(); > } >@@ -111,6 +179,41 @@ > > } > } >+ >+ public void contextStopping() { >+ this.lastContextStoppedTime.set(System.currentTimeMillis()); >+ >+ // save the current pool parameters to restore them later >+ int savedCorePoolSize = this.getCorePoolSize(); >+ TaskQueue taskQueue = getQueue() instanceof TaskQueue ? (TaskQueue) getQueue() : null; >+ if (taskQueue != null) { >+ // note by slaurent : quite oddly threadPoolExecutor.setCorePoolSize >+ // checks that queue.remainingCapacity()==0. I did not understand >+ // why, but to get the intended effect of waking up idle threads, I >+ // temporarily fake this condition. >+ taskQueue.setForcedRemainingCapacity(0); >+ } >+ >+ // setCorePoolSize(0) wakes idle threads >+ this.setCorePoolSize(0); >+ >+ // wait a little so that idle threads wake and poll the queue again, >+ // this time always with a timeout (queue.poll() instead of queue.take()) >+ // even if we did not wait enough, TaskQueue.take() takes care of timing out >+ // so that we are sure that all threads of the pool are renewed in a limited >+ // time, something like (threadKeepAlive + longest request time) >+ try { >+ Thread.sleep(200L); >+ } catch (InterruptedException e) { >+ //yes, ignore >+ } >+ >+ if (taskQueue != null) { >+ // ok, restore the state of the queue and pool >+ taskQueue.setForcedRemainingCapacity(null); >+ } >+ this.setCorePoolSize(savedCorePoolSize); >+ } > > private static class RejectHandler implements RejectedExecutionHandler { > @Override >Index: webapps/docs/config/context.xml >=================================================================== >--- webapps/docs/config/context.xml (revision 1006037) >+++ webapps/docs/config/context.xml (working copy) >@@ -441,13 +441,14 @@ > not specified, the default value of <code>false</code> will be used.</p> > </attribute> > >- <attribute name="clearReferencesThreadLocals" required="false"> >- <p>If <code>true</code>, Tomcat attempts to clear any ThreadLocal >- objects that are instances of classes loaded by this class loader. >- Failure to remove any such objects will result in a memory leak on web >- application stop, undeploy or reload. If not specified, the default >- value of <code>false</code> will be used since the clearing of the >- ThreadLocal objects is not performed in a thread-safe manner.</p> >+ <attribute name="renewThreadsWhenStoppingContext" required="false"> >+ <p>If <code>true</code>, when this context is stopped, Tomcat renews all >+ the threads from the thread pool that was used to serve this context. >+ This also requires that the >+ <code>ThreadLocalLeakPreventionListener</code> be configured in >+ <code>server.xml</code> and that the <code>threadRenewalDelay</code> >+ property of the <code>Executor</code> be >=0. If not specified, the >+ default value of <code>true</code> will be used.</p> > </attribute> > > <attribute name="processTlds" required="false"> >Index: webapps/docs/config/executor.xml >=================================================================== >--- webapps/docs/config/executor.xml (revision 1006037) >+++ webapps/docs/config/executor.xml (working copy) >@@ -106,6 +106,11 @@ > <p>(boolean) Whether minSpareThreads should be started when starting the Executor or not, > the default is <code>false</code></p> > </attribute> >+ <attribute name="threadRenewalDelay" required="false"> >+ <p>After a context is stopped, threads in the pool are renewed. To avoid renewing all threads at the same time, >+ this delay is observed between 2 threads being renewed. Value is in ms, default value is 1000ms. >+ If negative, threads are not renewed.</p> >+ </attribute> > </attributes> > >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
Attachments on
bug 49159
:
25411
|
26068
|
26074
|
26097
|
26108
|
26150
|
26156
|
26157