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);
+    }
+  }
 }