Fix permgen exhaustion due to classloader memory leak across server refresh.
http://code.google.com/p/google-web-toolkit/issues/detail?id=5486
-Explicitly call destroy() on the WebAppClassLoader
-Restart the server, not just the WebAppContext, on refresh
-Do a nasty hack to force unload JDBC drivers loaded in the WebAppClassLoader
-Port Tomcat's JreMemoryLeakPreventionListener
Review at http://gwt-code-reviews.appspot.com/1097801
Review by: rchandia@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9214 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JDBCUnloader.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JDBCUnloader.java
new file mode 100644
index 0000000..1f2e5e3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JDBCUnloader.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed 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 com.google.gwt.dev.shell.jetty;
+
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Enumeration;
+
+/**
+ * Workaround hack for the unfortunate architecture of JDBC drivers.
+ *
+ * If code loaded by WebAppClassLoader loads a JDBC driver, then it will pin the
+ * class loader across server refresh because java.sql.DriverManager is a system
+ * class and it maintains a static reference to any loaded drivers.
+ */
+public class JDBCUnloader {
+
+ public static void unload() {
+ Enumeration<Driver> drivers = DriverManager.getDrivers();
+
+ try {
+ while (drivers.hasMoreElements()) {
+ Driver driver = drivers.nextElement();
+ DriverManager.deregisterDriver(driver);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("Error unloading the JDBC Drivers", e);
+ }
+ }
+
+ private JDBCUnloader() {
+ }
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
index 3dedb92..1034031 100644
--- a/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
+++ b/dev/core/src/com/google/gwt/dev/shell/jetty/JettyLauncher.java
@@ -19,6 +19,7 @@
import com.google.gwt.core.ext.ServletContainerLauncher;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.dev.util.InstalledHelpInfo;
import org.mortbay.component.AbstractLifeCycle;
@@ -37,9 +38,17 @@
import java.io.File;
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.util.Iterator;
+import javax.imageio.ImageIO;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
/**
* A {@link ServletContainerLauncher} for an embedded Jetty server.
*/
@@ -231,7 +240,9 @@
Log.setLog(new JettyTreeLogger(branch));
try {
wac.stop();
+ server.stop();
wac.start();
+ server.start();
branch.log(TreeLogger.INFO, "Reload completed successfully");
} catch (Exception e) {
branch.log(TreeLogger.ERROR, "Unable to restart embedded Jetty server",
@@ -427,6 +438,8 @@
*/
private final ClassLoader systemClassLoader = Thread.currentThread().getContextClassLoader();
+ private WebAppClassLoaderExtension classLoader;
+
@SuppressWarnings("unchecked")
private WebAppContextWithReload(TreeLogger logger, String webApp,
String contextPath) {
@@ -443,14 +456,21 @@
@Override
protected void doStart() throws Exception {
- setClassLoader(new WebAppClassLoaderExtension());
+ classLoader = new WebAppClassLoaderExtension();
+ setClassLoader(classLoader);
super.doStart();
}
@Override
protected void doStop() throws Exception {
super.doStop();
+
+ Class<?> jdbcUnloader = classLoader.loadClass("com.google.gwt.dev.shell.jetty.JDBCUnloader");
+ java.lang.reflect.Method unload = jdbcUnloader.getMethod("unload");
+ unload.invoke(null);
+
setClassLoader(null);
+ classLoader.destroy();
}
}
@@ -514,6 +534,9 @@
// Setup our branch logger during startup.
Log.setLog(new JettyTreeLogger(branch));
+ // Force load some JRE singletons that can pin the classloader.
+ jreLeakPrevention(logger);
+
// Turn off XML validation.
System.setProperty("org.mortbay.xml.XmlParser.Validating", "false");
@@ -588,4 +611,117 @@
}
}
+ /**
+ * This is a modified version of JreMemoryLeakPreventionListener.java found
+ * in the Apache Tomcat project at
+ *
+ * http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/core/
+ * JreMemoryLeakPreventionListener.java
+ *
+ * Relevant part of the Tomcat NOTICE, retrieved from
+ * http://svn.apache.org/repos/asf/tomcat/trunk/NOTICE Apache Tomcat Copyright
+ * 1999-2010 The Apache Software Foundation
+ *
+ * This product includes software developed by The Apache Software Foundation
+ * (http://www.apache.org/).
+ */
+ private void jreLeakPrevention(TreeLogger logger) {
+ // Trigger a call to sun.awt.AppContext.getAppContext(). This will
+ // pin the common class loader in memory but that shouldn't be an
+ // issue.
+ ImageIO.getCacheDirectory();
+
+ /*
+ * Several components end up calling: sun.misc.GC.requestLatency(long)
+ *
+ * Those libraries / components known to trigger memory leaks due to
+ * eventual calls to requestLatency(long) are: -
+ * javax.management.remote.rmi.RMIConnectorServer.start()
+ */
+ try {
+ Class<?> clazz = Class.forName("sun.misc.GC");
+ Method method = clazz.getDeclaredMethod("requestLatency",
+ new Class[]{long.class});
+ method.invoke(null, Long.valueOf(3600000));
+ } catch (ClassNotFoundException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.gcDaemonFail", e);
+ } catch (SecurityException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.gcDaemonFail", e);
+ } catch (NoSuchMethodException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.gcDaemonFail", e);
+ } catch (IllegalArgumentException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.gcDaemonFail", e);
+ } catch (IllegalAccessException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.gcDaemonFail", e);
+ } catch (InvocationTargetException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.gcDaemonFail", e);
+ }
+
+ /*
+ * Calling getPolicy retains a static reference to the context class loader.
+ */
+ 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) {
+ logger.log(Type.WARN, "jreLeakPrevention.authPolicyFail", e);
+ } catch (IllegalArgumentException e) {
+ logger.log(Type.WARN, "jreLeakPrevention.authPolicyFail", e);
+ } catch (IllegalAccessException e) {
+ logger.log(Type.WARN, "jreLeakPrevention.authPolicyFail", e);
+ } catch (InvocationTargetException e) {
+ logger.log(Type.WARN, "jreLeakPrevention.authPolicyFail", e);
+ }
+
+ /*
+ * 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.
+ */
+ 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
+ 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) {
+ logger.log(Type.ERROR, "jreLeakPrevention.jarUrlConnCacheFail", e);
+ } catch (IOException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.jarUrlConnCacheFail", e);
+ }
+
+ /*
+ * Haven't got to the root of what is going on with this leak but if a web
+ * app is the first to make the calls below the web application class loader
+ * will be pinned in memory.
+ */
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ try {
+ factory.newDocumentBuilder();
+ } catch (ParserConfigurationException e) {
+ logger.log(Type.ERROR, "jreLeakPrevention.xmlParseFail", e);
+ }
+ }
}