Tomcat Guice/JDBC内存泄漏

Jef*_*len 49 java tomcat

由于Tomcat中的孤立线程,我遇到了内存泄漏.特别是,似乎Guice和JDBC驱动程序没有关闭线程.

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.
Run Code Online (Sandbox Code Playgroud)

我知道这与其他问题(例如这个问题)类似,但在我的情况下,"不要担心它"的答案是不够的,因为它给我带来了问题.我有CI服务器定期更新此应用程序,并且在6-10次重新加载后,CI服务器将挂起,因为Tomcat内存不足.

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

Bil*_*ill 52

我自己刚刚解决了这个问题.与其他一些答案相反,我不建议发出t.stop()命令.此方法已被弃用,并且有充分的理由.参考Oracle的原因.

但是,有一个解决方案可以删除此错误,而无需诉诸t.stop()...

您可以使用@Oso提供的大部分代码,只需替换以下部分即可

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

这应该正确关闭线程,错误应该消失.

  • `AbandonedConnectionCleanupThread`来自哪里? (4认同)
  • 啊真棒,我在5.1.22.该课程在5.1.23中介绍.+1谢谢 (2认同)
  • `AbandonedConnectionCleanupThread.shutdown();`已弃用.建议使用`AbandonedConnectionCleanupThread.checkedShutdown()`. (2认同)

Oso*_*Oso 15

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

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

我已经知道停止一个线程并不是一种处理它们的优雅方式,但是否则服务器在两次或三次部署之后会继续崩溃(并不总是可以重新启动应用服务器).

我创建的课程是:

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
                }
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

创建类后,将其注册到web.xml文件:

<web-app...
    <listener>
        <listener-class>path.to.ContextFinalizer</listener-class>
    </listener>
</web-app>
Run Code Online (Sandbox Code Playgroud)


Ste*_*n L 13

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

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

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
Run Code Online (Sandbox Code Playgroud)

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"
          classesToInitialize="com.mysql.jdbc.NonRegisteringDriver" />
Run Code Online (Sandbox Code Playgroud)

这假设您将MySQL JDBC驱动程序放入tomcat的lib目录而不是webapp.war的WEB-INF/lib目录中,因为重点是您的webapp 之前和之后加载驱动程序.

参考文献:


Law*_*Dol 11

从MySQL连接器5.1.23开始,提供了一种方法来关闭废弃的连接清理线程,AbandonedConnectionCleanupThread.shutdown.

但是,我们不希望在代码中直接依赖于其他不透明的JDBC驱动程序代码,因此我的解决方案是使用反射来查找类和方法,并在找到时调用它.以下完整的代码片段是所需的,在加载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();
    }
Run Code Online (Sandbox Code Playgroud)

如果JDBC驱动程序是MySQL连接器的最新版本,则它会干净地结束线程,否则什么也不做.

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

  • 我发现这是最好的答案,原因有两个:1)它不使用Thread.stop()和2)它不需要MySQL连接器上的显式app依赖. (2认同)

Ric*_*ace 6

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

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

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

}
Run Code Online (Sandbox Code Playgroud)