Tomcat Guice / JDBC内存泄漏

由于Tomcat中的孤线,我正在经历内存泄漏。 特别是,Guice和JDBC驱动程序似乎没有closures线程。

Aug 8, 2012 4:09:19 PM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads SEVERE: A web application appears to have started a thread named [com.google.inject.internal.util.$Finalizer] but has failed to stop it. This is very likely to create a memory leak. Aug 8, 2012 4:09:19 PM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads SEVERE: A web application appears to have started a thread named [Abandoned connection cleanup thread] but has failed to stop it. This is very likely to create a memory leak. 

我知道这跟其他问题(比如这个问题)是类似的,但就我而言,“不要担心”的答案是不够的,因为这对我造成了问题。 我有定期更新此应用程序的CI服务器,并在6-10重新加载后,CI服务器将挂起,因为Tomcat内存不足。

我需要能够清除这些孤立的线程,所以我可以更可靠地运行我的CI服务器。 任何帮助将不胜感激!

我只是自己处理这个问题。 相反的一些其他的答案,我不build议发行t.stop()命令。 这个方法已经被弃用,并且有很好的理由。 参考Oracle为此做的原因 。

但是,有一个解决scheme,无需诉诸t.stop()

您可以使用大部分代码@Oso提供,只需replace以下部分

 Set<Thread> threadSet = Thread.getAllStackTraces().keySet(); Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]); for(Thread t:threadArray) { if(t.getName().contains("Abandoned connection cleanup thread")) { synchronized(t) { t.stop(); //don't complain, it works } } } 

使用MySQL驱动程序提供的以下方法replace它:

 try { AbandonedConnectionCleanupThread.shutdown(); } catch (InterruptedException e) { logger.warn("SEVERE problem cleaning up: " + e.getMessage()); e.printStackTrace(); } 

这应该正确地closures线程,并且错误应该消失。

我有同样的问题,正如杰夫所说,“不要担心它的方法”是不是要走的路。

我做了一个ServletContextListener,当closures上下文时停止挂起的线程,然后在web.xml文件上注册这样的ContextListener。

我已经知道停止一个线程并不是处理这些线程的一种很好的方式,但是否则服务器会在两次或三次部署之后崩溃(不一定总能重新启动应用服务器)。

我创build的类是:

 public class ContextFinalizer implements ServletContextListener { private static final Logger LOGGER = LoggerFactory.getLogger(ContextFinalizer.class); @Override public void contextInitialized(ServletContextEvent sce) { } @Override public void contextDestroyed(ServletContextEvent sce) { Enumeration<Driver> drivers = DriverManager.getDrivers(); Driver d = null; while(drivers.hasMoreElements()) { try { d = drivers.nextElement(); DriverManager.deregisterDriver(d); LOGGER.warn(String.format("Driver %s deregistered", d)); } catch (SQLException ex) { LOGGER.warn(String.format("Error deregistering driver %s", d), ex); } } Set<Thread> threadSet = Thread.getAllStackTraces().keySet(); Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]); for(Thread t:threadArray) { if(t.getName().contains("Abandoned connection cleanup thread")) { synchronized(t) { t.stop(); //don't complain, it works } } } } } 

创build完类之后,将其注册到web.xml文件中:

 <web-app... <listener> <listener-class>path.to.ContextFinalizer</listener-class> </listener> </web-app> 

侵入性最小的解决方法是强制从Web应用程序的类加载器之外的代码初始化MySQL JDBC驱动程序。

在tomcat / conf / server.xml中,修改(在Server元素中):

 <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> 

 <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" classesToInitialize="com.mysql.jdbc.NonRegisteringDriver" /> 

这假设你将MySQL JDBC驱动程序放到tomcat的lib目录中,而不是在你的webapp.war的WEB-INF / lib目录中,因为整个过程是你的webapp 之前和之后加载驱动程序。

参考文献:

从MySQL连接器5.1.23开始有效,提供了一个closures放弃的连接清理线程的方法, AbandonedConnectionCleanupThread.shutdown

然而,我们并不希望在我们的代码中直接依赖于其他不透明的JDBC驱动程序代码,所以我的解决scheme是使用reflection来查找类和方法,如果find,就调用它。 以下完整的代码片段就是所有需要的,在加载JDBC驱动程序的类加载器的上下文中执行:

 try { Class<?> cls=Class.forName("com.mysql.jdbc.AbandonedConnectionCleanupThread"); Method mth=(cls==null ? null : cls.getMethod("shutdown")); if(mth!=null) { mth.invoke(null); } } catch (Throwable thr) { thr.printStackTrace(); } 

如果JDBC驱动程序是MySQL连接器的最新版本,则干净地结束线程,否则不会执行任何操作。

注意它必须在类加载器的上下文中执行,因为线程是一个静态引用; 如果在运行此代码时驱动程序类未处于或尚未卸载,则线程将不会运行以用于后续的JDBC交互。

我把上面的答案的最好的部分,并将其组合成一个容易扩展的类。 这将Oso的原始build议与Bill的驱动程序改进和Software Monkey的反思改进结合起来。 (我也喜欢Stephan L的简单回答,但有时修改Tomcat环境本身并不是一个好的select,特别是如果你必须处理自动缩放或迁移到另一个Web容器。

我不是直接引用类名,线程名称和停止方法,而是将它们封装到私有的内部ThreadInfo类中。 使用这些ThreadInfo对象的列表,可以包含更多麻烦的线程,使用相同的代码closures。 解决scheme比大多数人可能需要的要复杂一些,但是当你需要的时候应该更普遍地工作。

 import java.lang.reflect.Method; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Set; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Context finalization to close threads (MySQL memory leak prevention). * This solution combines the best techniques described in the linked Stack * Overflow answer. * @see <a href="https://stackoverflow.com/questions/11872316/tomcat-guice-jdbc-memory-leak">Tomcat Guice/JDBC Memory Leak</a> */ public class ContextFinalizer implements ServletContextListener { private static final Logger LOGGER = LoggerFactory.getLogger(ContextFinalizer.class); /** * Information for cleaning up a thread. */ private class ThreadInfo { /** * Name of the thread's initiating class. */ private final String name; /** * Cue identifying the thread. */ private final String cue; /** * Name of the method to stop the thread. */ private final String stop; /** * Basic constructor. * @param n Name of the thread's initiating class. * @param c Cue identifying the thread. * @param s Name of the method to stop the thread. */ ThreadInfo(final String n, final String c, final String s) { this.name = n; this.cue = c; this.stop = s; } /** * @return the name */ public String getName() { return this.name; } /** * @return the cue */ public String getCue() { return this.cue; } /** * @return the stop */ public String getStop() { return this.stop; } } /** * List of information on threads required to stop. This list may be * expanded as necessary. */ private List<ThreadInfo> threads = Arrays.asList( // Special cleanup for MySQL JDBC Connector. new ThreadInfo( "com.mysql.jdbc.AbandonedConnectionCleanupThread", //$NON-NLS-1$ "Abandoned connection cleanup thread", //$NON-NLS-1$ "shutdown" //$NON-NLS-1$ ) ); @Override public void contextInitialized(final ServletContextEvent sce) { // No-op. } @Override public final void contextDestroyed(final ServletContextEvent sce) { // Deregister all drivers. Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver d = drivers.nextElement(); try { DriverManager.deregisterDriver(d); LOGGER.info( String.format( "Driver %s deregistered", //$NON-NLS-1$ d ) ); } catch (SQLException e) { LOGGER.warn( String.format( "Failed to deregister driver %s", //$NON-NLS-1$ d ), e ); } } // Handle remaining threads. Set<Thread> threadSet = Thread.getAllStackTraces().keySet(); Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]); for (Thread t:threadArray) { for (ThreadInfo i:this.threads) { if (t.getName().contains(i.getCue())) { synchronized (t) { try { Class<?> cls = Class.forName(i.getName()); if (cls != null) { Method mth = cls.getMethod(i.getStop()); if (mth != null) { mth.invoke(null); LOGGER.info( String.format( "Connection cleanup thread %s shutdown successfully.", //$NON-NLS-1$ i.getName() ) ); } } } catch (Throwable thr) { LOGGER.warn( String.format( "Failed to shutdown connection cleanup thread %s: ", //$NON-NLS-1$ i.getName(), thr.getMessage() ) ); thr.printStackTrace(); } } } } } } } 

我更进一步从Oso,改进了两点上面的代码:

  1. 将Finalizer线程添加到需要检查的检查:

     for(Thread t:threadArray) { if(t.getName().contains("Abandoned connection cleanup thread") || t.getName().matches("com\\.google.*Finalizer") ) { synchronized(t) { logger.warn("Forcibly stopping thread to avoid memory leak: " + t.getName()); t.stop(); //don't complain, it works } } } 
  2. 睡一会儿,给线程时间停止。 没有这个,tomcat不停地抱怨。

     try { Thread.sleep(1000); } catch (InterruptedException e) { logger.debug(e.getMessage(), e); } 

比尔的解决scheme看起来不错,但是我直接在MySQL bug报告中find另一个解决scheme:

[2013年6月5日17时12]克里斯托弗舒尔茨这是一个更好的解决方法,直到其他更改。

启用Tomcat的JreMemoryLeakPreventionListener(在Tomcat 7上默认启用),并将该属性添加到元素:

classesToInitialize = “com.mysql.jdbc.NonRegisteringDriver”

如果已经设置了“classesToInitialize”,只需将NonRegisteringDriver添加到由逗号分隔的现有值。

答案是:

[8 Jun 2013 21:33] Marko Asplund我用JreMemoryLeakPreventionListener / classesToInitialize解决方法(Tomcat 7.0.39 + MySQL Connector / J 5.1.25)做了一些testing。

在应用变通方法之前,线程转储在多次重新部署webapp之后列出了多个AbandonedConnectionCleanupThread实例。 应用变通办法之后,只有一个AbandonedConnectionCleanupThread实例。

不过,我不得不修改我的应用程序,并将MySQL驱动程序从webapp移动到Tomcat lib。 否则,类加载器无法在Tomcat启动时加载com.mysql.jdbc.NonRegisteringDriver。

我希望这对那些仍在为这个问题而斗争的人有所帮助。

请参阅为防止内存泄漏,JDBC驱动程序已被强制取消注册 。 Bill的答案注销了所有Driver实例以及可能属于其他Web应用程序的实例。 我已经扩展了Bill的答案,检查Driver实例是否属于正确的ClassLoader

这里是结果代码(在一个单独的方法,因为我的contextDestroyed有其他的事情要做):

 // See https://stackoverflow.com/questions/25699985/the-web-application-appears-to-have-started-a-thread-named-abandoned-connect // and // https://stackoverflow.com/questions/3320400/to-prevent-a-memory-leak-the-jdbc-driver-has-been-forcibly-unregistered/23912257#23912257 private void avoidGarbageCollectionWarning() { ClassLoader cl = Thread.currentThread().getContextClassLoader(); Enumeration<Driver> drivers = DriverManager.getDrivers(); Driver d = null; while (drivers.hasMoreElements()) { try { d = drivers.nextElement(); if(d.getClass().getClassLoader() == cl) { DriverManager.deregisterDriver(d); logger.info(String.format("Driver %s deregistered", d)); } else { logger.info(String.format("Driver %s not deregistered because it might be in use elsewhere", d.toString())); } } catch (SQLException ex) { logger.warning(String.format("Error deregistering driver %s, exception: %s", d.toString(), ex.toString())); } } try { AbandonedConnectionCleanupThread.shutdown(); } catch (InterruptedException e) { logger.warning("SEVERE problem cleaning up: " + e.getMessage()); e.printStackTrace(); } } 

我不知道是否调用AbandonedConnectionCleanupThread.shutdown()是安全的。 它可以干扰其他Web应用程序吗? 我希望不会,因为AbandonedConnectionCleanupThread.run()方法不是静态的,而是AbandonedConnectionCleanupThread.shutdown()方法。

看来这是在5.1.41固定的。 您可以将Connector / J升级到5.1.41或更高版本。 https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-1-41.html

AbandonedConnectionCleanupThread的实现现在已经得到改进,现在有四种方法可以让开发者处理这种情况:

  • 当使用默认的Tomcatconfiguration并将Connector / J jar放入本地库目录时,Connector / J中的新内置应用程序检测器现在会在5秒内检测到Web应用程序停止并终止AbandonedConnectionCleanupThread。 任何关于线程不可阻挡的不必要的警告也是可以避免的。 如果将Connector / J jar放入全局库目录中,线程将保持运行状态,直到卸载JVM。

  • 当Tomcat的上下文被configuration为属性clearReferencesStopThreads =“true”时,当应用程序停止时,Tomcat将停止所有派生的线程,除非连接器/ J正与其他Web应用程序共享,在这种情况下,Connector / J现在受到保护,由Tomcat停止; 关于不可停止的线程的警告仍然发布到Tomcat的错误日志中。

  • 当在每个上下文销毁的AbandonedConnectionCleanupThread.checkedShutdown()的Web应用程序中实现ServletContextListener时,如果驱动程序可能与其他应用程序共享,那么Connector / J现在再次跳过此操作。 在这种情况下,没有关于线程被阻止的警告被发送到Tomcat的错误日志。

  • 当调用AbandonedConnectionCleanupThread.uncheckedShutdown()时,即使Connector / J与其他应用程序共享,AbandonedConnectionCleanupThread也会closures。 但是,之后可能无法重新启动线程。

如果你看源代码,他们在线程上调用了setDeamon(true),所以它不会阻塞closures。

 Thread t = new Thread(r, "Abandoned connection cleanup thread"); t.setDaemon(true);