blob: 46bd0b2be8cf4972e9dc59821fd5442e913a56a3 [file] [log] [blame]
/*
* 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.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.sql.DriverManager;
import java.util.StringTokenizer;
import java.util.concurrent.ForkJoinPool;
import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.startup.SafeForkJoinWorkerThreadFactory;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.compat.JreCompat;
import org.apache.tomcat.util.compat.JreVendor;
import org.apache.tomcat.util.res.StringManager;
import org.w3c.dom.Document;
import org.w3c.dom.ls.DOMImplementationLS;
/**
* Provide a workaround for known places where the Java Runtime environment can
* cause a memory leak or lock files.
* <p>
* Memory leaks occur when JRE code uses
* the context class loader to load a singleton as this will cause a memory leak
* if a web application class loader happens to be the context class loader at
* the time. The work-around is to initialise these singletons when Tomcat's
* common class loader is the context class loader.
* <p>
* Locked files usually occur when a resource inside a JAR is accessed without
* first disabling Jar URL connection caching. The workaround is to disable this
* caching by default.
*/
public class JreMemoryLeakPreventionListener implements LifecycleListener {
private static final Log log =
LogFactory.getLog(JreMemoryLeakPreventionListener.class);
private static final StringManager sm =
StringManager.getManager(Constants.Package);
private static final String FORK_JOIN_POOL_THREAD_FACTORY_PROPERTY =
"java.util.concurrent.ForkJoinPool.common.threadFactory";
/**
* Protect against the memory leak caused when the first call to
* <code>sun.awt.AppContext.getAppContext()</code> is triggered by a web
* application. Defaults to <code>false</code> since
* {@link java.beans.Introspector#flushCaches()} no longer uses AppContext
* from 1.7.0_02 onwards. Also, from 1.7.0_25 onwards, calling this method
* requires a graphical environment and starts an AWT thread.
*/
private boolean appContextProtection = false;
public boolean isAppContextProtection() { return appContextProtection; }
public void setAppContextProtection(boolean appContextProtection) {
this.appContextProtection = appContextProtection;
}
/**
* Protect against the memory leak caused when the first call to
* <code>java.awt.Toolkit.getDefaultToolkit()</code> is triggered
* by a web application. Defaults to <code>false</code> because a new
* Thread is launched.
*/
private boolean awtThreadProtection = false;
public boolean isAWTThreadProtection() { return awtThreadProtection; }
public void setAWTThreadProtection(boolean awtThreadProtection) {
this.awtThreadProtection = awtThreadProtection;
}
/**
* Protect against the memory leak caused when the first call to
* <code>sun.misc.GC.requestLatency(long)</code> is triggered by a web
* application. This first call will start a GC Daemon thread with the
* thread's context class loader configured to be the web application class
* loader. Defaults to <code>true</code>.
*/
private boolean gcDaemonProtection = true;
public boolean isGcDaemonProtection() { return gcDaemonProtection; }
public void setGcDaemonProtection(boolean gcDaemonProtection) {
this.gcDaemonProtection = gcDaemonProtection;
}
/**
* Protect against the memory leak caused when the first call to
* <code>javax.security.auth.Policy</code> is triggered by a web
* application. This first call populate a static variable with a reference
* to the context class loader. Defaults to <code>true</code>.
*/
private boolean securityPolicyProtection = true;
public boolean isSecurityPolicyProtection() {
return securityPolicyProtection;
}
public void setSecurityPolicyProtection(boolean securityPolicyProtection) {
this.securityPolicyProtection = securityPolicyProtection;
}
/**
* Protects against the memory leak caused when the first call to
* <code>javax.security.auth.login.Configuration</code> is triggered by a
* web application. This first call populate a static variable with a
* reference to the context class loader. Defaults to <code>true</code>.
*/
private boolean securityLoginConfigurationProtection = true;
public boolean isSecurityLoginConfigurationProtection() {
return securityLoginConfigurationProtection;
}
public void setSecurityLoginConfigurationProtection(
boolean securityLoginConfigurationProtection) {
this.securityLoginConfigurationProtection = securityLoginConfigurationProtection;
}
/**
* Protect against the memory leak, when the initialization of the
* Java Cryptography Architecture is triggered by initializing
* a MessageDigest during web application deployment.
* This will occasionally start a Token Poller thread with the thread's
* context class loader equal to the web application class loader.
* Instead we initialize JCA early.
* Defaults to <code>true</code>.
*/
private boolean tokenPollerProtection = true;
public boolean isTokenPollerProtection() { return tokenPollerProtection; }
public void setTokenPollerProtection(boolean tokenPollerProtection) {
this.tokenPollerProtection = tokenPollerProtection;
}
/**
* Protect against resources being read for JAR files and, as a side-effect,
* the JAR file becoming locked. Note this disables caching for all
* {@link URLConnection}s, regardless of type. Defaults to
* <code>true</code>.
*/
private boolean urlCacheProtection = true;
public boolean isUrlCacheProtection() { return urlCacheProtection; }
public void setUrlCacheProtection(boolean urlCacheProtection) {
this.urlCacheProtection = urlCacheProtection;
}
/**
* XML parsing can pin a web application class loader in memory. There are
* multiple root causes for this. Some of these are particularly nasty as
* profilers may not identify any GC roots related to the leak. For example,
* with YourKit you need to ensure that HPROF format memory snapshots are
* used to be able to trace some of the leaks.
*/
private boolean xmlParsingProtection = true;
public boolean isXmlParsingProtection() { return xmlParsingProtection; }
public void setXmlParsingProtection(boolean xmlParsingProtection) {
this.xmlParsingProtection = xmlParsingProtection;
}
/**
* <code>com.sun.jndi.ldap.LdapPoolManager</code> class spawns a thread when
* it is initialized if the system property
* <code>com.sun.jndi.ldap.connect.pool.timeout</code> is greater than 0.
* That thread inherits the context class loader of the current thread, so
* that there may be a web application class loader leak if the web app
* is the first to use <code>LdapPoolManager</code>.
*/
private boolean ldapPoolProtection = true;
public boolean isLdapPoolProtection() { return ldapPoolProtection; }
public void setLdapPoolProtection(boolean ldapPoolProtection) {
this.ldapPoolProtection = ldapPoolProtection;
}
/**
* The first access to {@link DriverManager} will trigger the loading of
* all {@link java.sql.Driver}s in the the current class loader. The web
* application level memory leak protection can take care of this in most
* cases but triggering the loading here has fewer side-effects.
*/
private boolean driverManagerProtection = true;
public boolean isDriverManagerProtection() {
return driverManagerProtection;
}
public void setDriverManagerProtection(boolean driverManagerProtection) {
this.driverManagerProtection = driverManagerProtection;
}
/**
* {@link ForkJoinPool#commonPool()} creates a thread pool that, by default,
* creates threads that retain references to the thread context class
* loader.
*/
private boolean forkJoinCommonPoolProtection = true;
public boolean getForkJoinCommonPoolProtection() {
return forkJoinCommonPoolProtection;
}
public void setForkJoinCommonPoolProtection(boolean forkJoinCommonPoolProtection) {
this.forkJoinCommonPoolProtection = forkJoinCommonPoolProtection;
}
/**
* List of comma-separated fully qualified class names to load and initialize during
* the startup of this Listener. This allows to pre-load classes that are known to
* provoke classloader leaks if they are loaded during a request processing.
*/
private String classesToInitialize = null;
public String getClassesToInitialize() {
return classesToInitialize;
}
public void setClassesToInitialize(String classesToInitialize) {
this.classesToInitialize = classesToInitialize;
}
@Override
public void lifecycleEvent(LifecycleEvent event) {
// Initialise these classes when Tomcat starts
if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try
{
// Use the system classloader as the victim for all this
// ClassLoader pinning we're about to do.
Thread.currentThread().setContextClassLoader(
ClassLoader.getSystemClassLoader());
/*
* First call to this loads all drivers in the current class
* loader
*/
if (driverManagerProtection) {
DriverManager.getDrivers();
}
/*
* Several components end up calling:
* sun.awt.AppContext.getAppContext()
*
* Those libraries / components known to trigger memory leaks
* due to eventual calls to getAppContext() are:
* - Google Web Toolkit via its use of javax.imageio
* - Tomcat via its use of java.beans.Introspector.flushCaches()
* in 1.7.0 to 1.7.0_01. From 1.7.0_02 onwards use of
* AppContext by Introspector.flushCaches() was replaced with
* ThreadGroupContext
* - others TBD
*
* From 1.7.0_25 onwards, a call to
* sun.awt.AppContext.getAppContext() results in a thread being
* started named AWT-AppKit that requires a graphic environment
* to be available.
*/
// Trigger a call to sun.awt.AppContext.getAppContext(). This
// will pin the system class loader in memory but that shouldn't
// be an issue.
if (appContextProtection && !JreCompat.isJre8Available()) {
ImageIO.getCacheDirectory();
}
// Trigger the creation of the AWT (AWT-Windows, AWT-XAWT,
// etc.) thread
if (awtThreadProtection && !JreCompat.isJre9Available()) {
java.awt.Toolkit.getDefaultToolkit();
}
/*
* Several components end up calling
* sun.misc.GC.requestLatency(long) which creates a daemon
* thread without setting the TCCL.
*
* Those libraries / components known to trigger memory leaks
* due to eventual calls to requestLatency(long) are:
* - javax.management.remote.rmi.RMIConnectorServer.start()
*
* Note: Long.MAX_VALUE is a special case that causes the thread
* to terminate
*
*/
if (gcDaemonProtection && !JreCompat.isJre9Available()) {
try {
Class<?> clazz = Class.forName("sun.misc.GC");
Method method = clazz.getDeclaredMethod(
"requestLatency",
new Class[] {long.class});
method.invoke(null, Long.valueOf(Long.MAX_VALUE - 1));
} catch (ClassNotFoundException e) {
if (JreVendor.IS_ORACLE_JVM) {
log.error(sm.getString(
"jreLeakListener.gcDaemonFail"), e);
} else {
log.debug(sm.getString(
"jreLeakListener.gcDaemonFail"), e);
}
} catch (SecurityException e) {
log.error(sm.getString("jreLeakListener.gcDaemonFail"),
e);
} catch (NoSuchMethodException e) {
log.error(sm.getString("jreLeakListener.gcDaemonFail"),
e);
} catch (IllegalArgumentException e) {
log.error(sm.getString("jreLeakListener.gcDaemonFail"),
e);
} catch (IllegalAccessException e) {
log.error(sm.getString("jreLeakListener.gcDaemonFail"),
e);
} catch (InvocationTargetException e) {
ExceptionUtils.handleThrowable(e.getCause());
log.error(sm.getString("jreLeakListener.gcDaemonFail"),
e);
}
}
/*
* Calling getPolicy retains a static reference to the context
* class loader.
*/
if (securityPolicyProtection && !JreCompat.isJre8Available()) {
try {
// Policy.getPolicy();
Class<?> policyClass = Class
.forName("javax.security.auth.Policy");
Method method = policyClass.getMethod("getPolicy");
method.invoke(null);
} catch(ClassNotFoundException e) {
// Ignore. The class is deprecated.
} catch(SecurityException e) {
// Ignore. Don't need call to getPolicy() to be
// successful, just need to trigger static initializer.
} catch (NoSuchMethodException e) {
log.warn(sm.getString("jreLeakListener.authPolicyFail"),
e);
} catch (IllegalArgumentException e) {
log.warn(sm.getString("jreLeakListener.authPolicyFail"),
e);
} catch (IllegalAccessException e) {
log.warn(sm.getString("jreLeakListener.authPolicyFail"),
e);
} catch (InvocationTargetException e) {
ExceptionUtils.handleThrowable(e.getCause());
log.warn(sm.getString("jreLeakListener.authPolicyFail"),
e);
}
}
/*
* Initializing javax.security.auth.login.Configuration retains a static reference to the context
* class loader.
*/
if (securityLoginConfigurationProtection && !JreCompat.isJre8Available()) {
try {
Class.forName("javax.security.auth.login.Configuration", true, ClassLoader.getSystemClassLoader());
} catch(ClassNotFoundException e) {
// Ignore
}
}
/*
* Creating a MessageDigest during web application startup
* initializes the Java Cryptography Architecture. Under certain
* conditions this starts a Token poller thread with TCCL equal
* to the web application class loader.
*
* Instead we initialize JCA right now.
*
* Fixed in Java 9 onwards (from early access build 133)
*/
if (tokenPollerProtection && !JreCompat.isJre9Available()) {
java.security.Security.getProviders();
}
/*
* Several components end up opening JarURLConnections without
* first disabling caching. This effectively locks the file.
* Whilst more noticeable and harder to ignore on Windows, it
* affects all operating systems.
*
* Those libraries/components known to trigger this issue
* include:
* - log4j versions 1.2.15 and earlier
* - javax.xml.bind.JAXBContext.newInstance()
*/
// Set the default URL caching policy to not to cache
if (urlCacheProtection) {
try {
// Doesn't matter that this JAR doesn't exist - just as
// long as the URL is well-formed
URL url = new URL("jar:file://dummy.jar!/");
URLConnection uConn = url.openConnection();
uConn.setDefaultUseCaches(false);
} catch (MalformedURLException e) {
log.error(sm.getString(
"jreLeakListener.jarUrlConnCacheFail"), e);
} catch (IOException e) {
log.error(sm.getString(
"jreLeakListener.jarUrlConnCacheFail"), e);
}
}
/*
* Fixed in Java 9 onwards (from early access build 133)
*/
if (xmlParsingProtection && !JreCompat.isJre9Available()) {
// There are two known issues with XML parsing that affect
// Java 7+. The issues both relate to cached Exception
// instances that retain a link to the TCCL via the
// backtrace field. Note that YourKit only shows this field
// when using the HPROF format memory snapshots.
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58486
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
// Issue 1
// com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl
Document document = documentBuilder.newDocument();
document.createElement("dummy");
DOMImplementationLS implementation =
(DOMImplementationLS)document.getImplementation();
implementation.createLSSerializer().writeToString(document);
// Issue 1
// com.sun.org.apache.xerces.internal.dom.DOMNormalizer
document.normalize();
} catch (ParserConfigurationException e) {
log.error(sm.getString("jreLeakListener.xmlParseFail"),
e);
}
}
if (ldapPoolProtection && !JreCompat.isJre9Available()) {
try {
Class.forName("com.sun.jndi.ldap.LdapPoolManager");
} catch (ClassNotFoundException e) {
if (JreVendor.IS_ORACLE_JVM) {
log.error(sm.getString(
"jreLeakListener.ldapPoolManagerFail"), e);
} else {
log.debug(sm.getString(
"jreLeakListener.ldapPoolManagerFail"), e);
}
}
}
/*
* Present in Java 8 onwards
*/
if (forkJoinCommonPoolProtection & JreCompat.isJre8Available()) {
// Don't override any explicitly set property
if (System.getProperty(FORK_JOIN_POOL_THREAD_FACTORY_PROPERTY) == null) {
System.setProperty(FORK_JOIN_POOL_THREAD_FACTORY_PROPERTY,
SafeForkJoinWorkerThreadFactory.class.getName());
}
}
if (classesToInitialize != null) {
StringTokenizer strTok =
new StringTokenizer(classesToInitialize, ", \r\n\t");
while (strTok.hasMoreTokens()) {
String classNameToLoad = strTok.nextToken();
try {
Class.forName(classNameToLoad);
} catch (ClassNotFoundException e) {
log.error(
sm.getString("jreLeakListener.classToInitializeFail",
classNameToLoad), e);
// continue with next class to load
}
}
}
} finally {
Thread.currentThread().setContextClassLoader(loader);
}
}
}
}