Tomcat Guice/JDBC 内存泄漏

Posted

技术标签:

【中文标题】Tomcat Guice/JDBC 内存泄漏【英文标题】:Tomcat Guice/JDBC Memory Leak 【发布时间】:2012-08-06 00:33:39 【问题描述】:

由于 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.

我知道这与其他问题类似(例如this one),但就我而言,“别担心”的答案是不够的,因为它给我带来了问题。我有 CI 服务器,它会定期更新这个应用程序,并且在 6-10 次重新加载后,CI 服务器会因为 Tomcat 内存不足而挂起。

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

【问题讨论】:

确定这些是OOM错误的原因吗? JDBC 问题通过在上下文销毁事件上使用上下文侦听器终止线程并将驱动程序放入应用程序的 lib 中来解决,因此类加载是在应用程序的上下文中完成的,而不是在容器的上下文中。 谢谢。我对这个领域很陌生,所以我完全不确定这是 OOM 错误的原因,但这是我在重新部署这个 Web 应用程序时在 Tomcat 日志中得到的唯一可疑注释。您推荐的有关查找源或正确使用 contextListener 的任何提示?在快速谷歌之后,我没有发现任何明显相关的教程,但如果你能指出我正确的方向,我很乐意阅读这个问题。 要使用上下文侦听器卸载驱动程序,请在此处查看答案:***.com/questions/3320400/… 我遇到了同样的问题。 Tomcat 显示线程“已放弃的连接清理线程”的次数与重新启动 web 应用程序的次数一样多,并且取消注册驱动程序对我没有任何帮助......有什么好消息吗? 也许我不是很聪明,但我没有看到与番石榴问题相关的答案(大多数答案都集中在 mysql conn pool),这会影响我(在添加应用程序之前在重新加载/停止时清理得很好):我最终为 perm gen 空间设置了一个更高的限制,因此我可以在 tomcat 重新启动之间等待更多时间。看到 Google 库出现内存泄漏,真是令人难过…… 【参考方案1】:

侵入性最小的解决方法是从 webapp 的类加载器之外的代码强制初始化 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-connector-java-8.0.x,请改用 com.mysql.cj.jdbc.NonRegisteringDriver

这假设您将 MySQL JDBC 驱动程序放在 tomcat 的 lib 目录中,而不是放在 webapp.war 的 WEB-INF/lib 目录中,因为重点是在 之前 加载驱动程序并且独立于您的网络应用程序。

参考资料:

http://bugs.mysql.com/bug.php?id=68556#c400606

http://tomcat.apache.org/tomcat-7.0-doc/config/listeners.html#JRE_Memory_Leak_Prevention_Listener_-_org.apache.catalina.core.JreMemoryLeakPreventionListener

http://markmail.org/message/dmvlkps7lbgpngil

com.mysql.jdbc.NonRegisteringDriver 源 v5.1

com.mysql.cj.jdbc.NonRegisteringDriver源码v8.0 Changes in connector/J v8.0

【讨论】:

不幸的是,这对我不起作用。我正在使用 TomEE 1.6.0 (Tomcat 7.0.47) 和 MySQL 驱动程序 5.1.27。最后我选择了基于 AbandonedConnectionCleanupThread 类的解决方案。线程杀戮也对我有用,但我更喜欢直接连接到问题根源的解决方案,即 MySQL JDBC 驱动程序本身。 @MiklosKrivan 您确实将 JDBC 驱动程序 .jar 文件放在了 tomcat 的 lib 目录中,而不是放在 .war 的 WEB-INF/lib 中,对吗?我相信 JreMemoryLeakPreventionListener 需要第一种方法才能工作,并假设后一种方法需要在被关闭一次后重新启动 AbandonedConnectionCleanupThread 线程(它是从 JDBC 驱动程序本身中的静态初始化程序启动的)。 是的,我总是将 JDBC 驱动程序放入 Tomcat 的库中,因为我使用数据源,而且我更喜欢容器管理数据库池而不是应用程序。这在企业 Java 应用服务器以及我通常使用的服务器中都是必需的。您的建议与 JDBC 驱动程序位于 Tomcat 的库中的情况有关。这就是我写评论的原因。也许我误解了什么? @MiklosKrivan 不会将 JDBC 驱动程序放在 Tomcat 的 lib 目录中,然后调用 AbandonedConnectionCleanupThread.shutdown() 会导致该线程永远停止运行,直到重新启动 tomcat,停止清理它本来会做的连接在其他或重新部署的 web 应用程序的后台?如果您的 web 应用程序表现良好并且总是正确关闭它们的连接,当然这可能并不重要,但还没有研究 AbandonedConnectionCleanupThread 实际执行的细节。【参考方案2】:

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

但是,我们不希望我们的代码直接依赖于其他不透明的 JDBC 驱动程序代码,所以我的解决方案是使用反射来查找类和方法,如果找到就调用它。以下完整的代码 sn-p 就是所有需要的,在加载 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 交互。

【讨论】:

我发现这是最好的答案,原因有两个:1)它不使用 Thread.stop() 和 2)它不需要对 MySQL 连接器的显式应用程序依赖。 【参考方案3】:

这似乎已在 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 的实现,因此现在有四种方式供开发人员处理这种情况:

当使用默认 Tomcat 配置并将 Connector/J jar 放入本地库目录时,Connector/J 中新的内置应用程序检测器现在可以在 5 秒内检测到 Web 应用程序的停止,并且杀死 AbandonedConnectionCleanupThread。也避免了关于线程不可停止的任何不必要的警告。如果将 Connector/J jar 放入全局库目录,则线程会一直运行,直到 JVM 被卸载。

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

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

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

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

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

【讨论】:

【参考方案4】:

我也遇到过同样的问题,正如 Jeff 所说,“不要担心它的方法”不是要走的路。

我做了一个 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
                
            
        
    


创建类后,然后在web.xml文件中注册:

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

【讨论】:

这是一个糟糕的解决方案,原因有三个。 1. 它使用 t.stop(),它不仅被弃用,而且计划被移除。 2. 它依赖于一个可以改变的任意程序字符串(“Abandoned connection cleanup thread”)。 3. 它在Thread 对象上同步,这可能会导致意外的死锁。【参考方案5】:

见To prevent a memory leak, the JDBC Driver has been forcibly unregistered。 Bill 的回答取消了所有 Driver 实例以及可能属于其他 Web 应用程序的实例的注册。我通过检查 Driver 实例是否属于正确的 ClassLoader 来扩展 Bill 的答案。

这是生成的代码(在单独的方法中,因为我的contextDestroyed 有其他事情要做):

// See https://***.com/questions/25699985/the-web-application-appears-to-have-started-a-thread-named-abandoned-connect
// and
// https://***.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() 方法是。

【讨论】:

【参考方案6】:

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

但是有一个解决方案可以消除此错误,而无需求助于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
        
    

使用MySQL驱动提供的如下方法替换:

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

这应该会正确关闭线程,并且错误应该消失。

【讨论】:

最初很难找到。没有javadocs,很少参考。我发现它是通过 tomcat 上的错误报告查看的。这是来自 oracle 本身的间接参考。 docs.oracle.com/cd/E17952_01/connector-j-relnotes-en/… 太棒了,我在 5.1.22。该类是在 5.1.23 中引入的。 +1 谢谢 (抱歉 cmets 没有很好地格式化这个......)您在“Oracle 的原因”上的链接应该更新为:docs.oracle.com/javase/6/docs/technotes/guides/concurrency/… 我会添加一个关于放置位置的建议。使用 spring 我在我的 web.xml com.mypackage.web.context.MyContextLoaderListener 这个类扩展了 org.springframework.web.context.ContextLoaderListener 和在 contextDestroyed() 我这样做。 AbandonedConnectionCleanupThread.shutdown(); 已弃用。建议使用AbandonedConnectionCleanupThread.checkedShutdown() 自 2018-07-25 起,AbandonedConnectionCleanupThread.checkedShutdown() 也已弃用。我找不到关于这个类的任何文档。【参考方案7】:

Bill 的解决方案看起来不错,但是我直接在 MySQL 错误报告中找到了另一个解决方案:

[2013 年 6 月 5 日 17:12] 克里斯托弗·舒尔茨 在其他情况发生变化之前,这是一个更好的解决方法。

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

classesToInitialize="com.mysql.jdbc.NonRegisteringDriver"

如果“classesToInitialize”已在您的 上设置,只需将 NonRegisteringDriver 添加到以逗号分隔的现有值。

以及答案:

[2013 年 6 月 8 日 21:33] Marko Asplund 我使用 JreMemoryLeakPreventionListener / classesToInitialize 解决方法(Tomcat 7.0.39 + MySQL Connector/J 5.1.25)进行了一些测试。

在应用解决方法之前,线程转储列出了多次重新部署 web 应用后的多个 AbandonedConnectionCleanupThread 实例。应用解决方法后,只有一个 AbandonedConnectionCleanupThread 实例。

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

我希望它对所有仍在与这个问题作斗争的人有所帮助......

【讨论】:

【参考方案8】:

我提取了上面答案中最好的部分,并将它们组合成一个易于扩展的类。这结合了 Oso 的原始建议与 Bill 的驱动程序改进和 Software Monkey 的反射改进。 (我也喜欢 Stephan L 的简单回答,但有时修改 Tomcat 环境本身并不是一个好的选择,尤其是当您必须处理自动缩放或迁移到另一个 Web 容器时。)

我没有直接引用类名、线程名和停止方法,而是将它们封装在一个私有的内部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://***.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();
                        
                    
                
            
        
    


【讨论】:

【参考方案9】:

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

    将 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
            
        
    
    

    睡一会儿,让线程有时间停止。没有那个,tomcat一直在抱怨。

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

【讨论】:

让代码依赖于将来可能会在没有警告的情况下更改的任意字符串是个坏主意(“废弃的连接清理线程”)。 "这比让你的服务器泄露要好" :直到文本在某个可能遥远的将来发生变化,并且你的服务器再次开始泄露,并且你第一次意识到它当它崩溃时。 请随意提出更好的方法。不要误会我的意思,我讨厌变通方法,但如果您没有更优雅的解决方案,它们比他们修补的问题要好。 我愿意,它是 already posted,早在 2013 年 7 月。 你的回答修复了mysql泄漏问题,这方面比我的要好,但是谷歌的呢?

以上是关于Tomcat Guice/JDBC 内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

分析 ThreadLocal 内存泄漏问题

内存泄漏和内存溢出的区别

Android开发常见的Activity中内存泄漏及解决办法

OpenGL VBO 会泄漏内存吗?

PowerPoint 2010 内存泄漏?

Javascript的内存泄漏分析