簡體   English   中英

Tomcat Guice/JDBC 內存泄漏

[英]Tomcat Guice/JDBC Memory Leak

由於 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 服務器。 任何幫助,將不勝感激!

我只是自己處理了這個問題。 與其他一些答案相反,我不建議發出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();
}

這應該正確關閉線程,並且錯誤應該消失。

我遇到了同樣的問題,正如傑夫所說,“不要擔心它的方法”不是要走的路。

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

侵入性最小的解決方法是從 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 目錄中,因為重點是您的 webapp之前加載驅動程序並且獨立於您的 webapp。

參考:

從 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 交互運行。

我把上面答案中最好的部分組合成一個易於擴展的類。 這結合了 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();
                        }
                    }
                }
            }
        }
    }

}

我比 Oso 更進一步,在兩點改進了上面的代碼:

  1. 將終結器線程添加到需要終止檢查中:

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

Bill 的解決方案看起來不錯,但是我直接在 MySQL 錯誤報告中找到了另一個解決方案:

[2013 年 6 月 5 日 17:12] Christopher Schultz 這是一個更好的解決方法,直到其他事情發生變化。

啟用 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。

我希望它對所有仍在與此問題作斗爭的人有所幫助......

似乎這已在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 將在應用程序停止時停止所有產生的線程,除非 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);

請參閱為防止內存泄漏,JDBC 驅動程序已被強制注銷 Bill 的回答會注銷所有 Driver 實例以及可能屬於其他 Web 應用程序的實例。 我通過檢查 Driver 實例是否屬於正確的ClassLoader來擴展 Bill 的回答。

這是結果代碼(在一個單獨的方法中,因為我的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()方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM