Tomcat Guice/JDBC 内存泄漏

50

我在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.

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

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


这些肯定是导致OOM错误的原因吗?JDBC问题可以通过在上下文销毁事件上使用上下文监听器杀死线程,并将驱动程序放置在应用程序的lib中,以便类加载在应用程序的上下文中完成,而不是在容器中完成。 - Alfabravo
谢谢。我对这个领域还很陌生,所以我不确定这是否是OOM错误的原因,但当我重新部署这个Web应用程序时,在Tomcat的日志中,这是唯一可疑的提示。您有没有找到源头或者按照您的建议正确使用contextListener的任何提示?在快速搜索后,我没有发现任何明显相关的教程,但如果您能指点我正确的方向,我会很高兴阅读这个问题的相关内容。 - Jeff Allen
1
使用ContextListener卸载驱动程序,请查看以下答案:https://dev59.com/PnA75IYBdhLWcg3wYYFQ - Alfabravo
我正在遇到相同的问题。Tomcat会显示线程“Abandoned connection cleanup thread”,并且每次Web应用程序重新启动时都会出现这个线程。注销驱动程序对我没有帮助...有好消息吗? - Oso
也许是我不够聪明,但我没有看到与guava问题相关的答案(大多数答案都集中在mysql连接池上),这影响了我(在添加该内容之前,应用程序在重新加载/停止时清理得很好):最终我设置了更高的永久代空间限制,这样我就可以在tomcat重启时等待更长的时间。看到一个谷歌库有内存泄漏真是令人沮丧... - reallynice
我正在使用 MySql 驱动程序 5.1.36,但是我看到了这个错误。这个 bug 有被修复过吗? - Arjang
9个回答

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

使用MySQL驱动程序提供的以下方法进行替换:
try {
    AbandonedConnectionCleanupThread.shutdown();
} catch (InterruptedException e) {
    logger.warn("SEVERE problem cleaning up: " + e.getMessage());
    e.printStackTrace();
}

这应该可以正确地关闭线程,错误就会消失。

1
最初找到这个确实挺费劲的。没有Javadocs,资料也不多。我是在翻阅Tomcat的bug报告时发现了它。这里是Oracle官方的一个间接参考链接。http://docs.oracle.com/cd/E17952_01/connector-j-relnotes-en/news-5-1-23.html - Bill
2
哇,太棒了,我原来用的是5.1.22版本。这个类是在5.1.23版本中引入的。+1 谢谢! - Sotirios Delimanolis
1
你在“Oracle的原因”链接应该更新为: http://docs.oracle.com/javase/6/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html我建议将其放在以下位置。使用Spring,我在我的web.xml中有以下内容 <listener> <listener-class>com.mypackage.web.context.MyContextLoaderListener</listener-class> </listener>这个类扩展了org.springframework.web.context.ContextLoaderListener并在contextDestroyed()中执行。 - Daniele Segato
2
AbandonedConnectionCleanupThread.shutdown();已被弃用。建议使用AbandonedConnectionCleanupThread.checkedShutdown() - JRSofty
Connector/J的开发人员似乎不理解如何构建可以在这种情况下使用的驱动程序。https://bugs.mysql.com/bug.php?id=73876和https://bugs.mysql.com/bug.php?id=69526 - Christopher Schultz
显示剩余2条评论

15
我遇到了同样的问题,正如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对象上进行同步,可能会导致令人惊讶的死锁。 - Lawrence Dol

14

最不侵入性的解决方法是在网站应用程序类加载器之外的代码中强制初始化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 目录中,因为整个要点是在 web 应用程序之前和独立于其之前加载驱动程序。

参考文献:


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

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

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

请注意,在类加载器的上下文中执行此操作,因为该线程是静态引用;如果在运行此代码时未卸载或已经卸载驱动程序类,则该线程将无法运行以进行后续的JDBC交互。


2
我认为这是最好的答案,有两个原因:1)它不使用Thread.stop();2)它不需要显式地依赖于MySQL连接器。 - Jason Nichols

6
我将以上回答的最佳部分结合起来,形成了一个易于扩展的类。这将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://dev59.com/gGct5IYBdhLWcg3wuPu6">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();
                        }
                    }
                }
            }
        }
    }

}

2

我从Oso的基础上进一步改进了代码,主要有以下两点:

  1. Added the Finalizer thread to the need-to-kill check:

    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. Sleep for a little while to give threads time to stop. Without that, tomcat kept complaining.

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

将代码依赖于任意可能在未来发生变化且没有警告的字符串(“Abandoned connection cleanup thread”)是一个不好的想法。 - Lawrence Dol
“这比你的服务器泄漏要好。”:直到文本在可能遥远的未来发生变化,而你的服务器再次开始泄漏,并且第一次意识到它是在它崩溃时。 - Lawrence Dol
随意提出更好的方法。别误会,我讨厌权宜之计,但如果你没有更优雅的解决方案,它们比它们修补的问题要好。 - Bruno Medeiros
我已经做了,并且它已经在2013年7月发布了(https://dev59.com/gGct5IYBdhLWcg3wuPu6#17892862)。 - Lawrence Dol
你的答案修复了MySQL泄漏问题,在这方面比我的更好,但是谷歌的呢? - Bruno Medeiros
你的答案可能是Google线程中唯一可能的答案(我在问题中错过了这一点)。所以,如果是我,我会在JDBC驱动程序线程中使用我的解决方案,并进一步研究Google线程,如果绝对没有其他更好的方法,可能会采用你的答案。所以,在这一点上,我们同意,一个粗糙、脆弱的解决方案比没有解决方案要好。当然,正如我所说,Thread.stop()计划在Java10中被删除,因此它的寿命有限。 - Lawrence Dol

2

比尔的解决方案看起来很好,但我在MySQL错误报告中找到了另一个解决方案:

[2013年6月5日17:12] Christopher Schultz 这是一个较好的解决方法,直到其他更改发生。

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

classesToInitialize="com.mysql.jdbc.NonRegisteringDriver"

如果您的 <Context> 元素已经设置了 "classesToInitialize",只需将 NonRegisteringDriver 添加到现有值中,用逗号隔开即可。

答案如下:

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

在应用程序重新部署多次后,线程转储列出了多个 AbandonedConnectionCleanupThread 实例。在应用该解决方案后,仅存在一个 AbandonedConnectionCleanupThread 实例。

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

我希望这能帮助所有仍在解决此问题的人...


2

看起来这个问题在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卸载。

  • 当Tomcat的上下文配置具有属性clearReferencesStopThreads =“true”时,当应用程序停止时,Tomcat将停止所有生成的线程,除非Connector/J正在与其他Web应用程序共享,在这种情况下,Connector/J现在受到保护,以防止Tomcat不适当地停止;仍然会在Tomcat的错误日志中发出有关不可停止线程的警告。

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

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

如果您查看源代码,它在线程上调用了setDeamon(true),因此它不会阻塞关闭。

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

1

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

以下是结果代码(在单独的方法中,因为我的contextDestroyed还有其他事情要做):

// See https://dev59.com/2F8e5IYBdhLWcg3wfaUy
// and
// https://dev59.com/PnA75IYBdhLWcg3wYYFQ#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()方法是静态的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接