web-dev-qa-db-ja.com

Tomcat Guice / JDBCメモリリーク

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回リロードした後、Tomcatのメモリ不足のためCIサーバーがハングします。

CIサーバーをより確実に実行できるように、これらの孤立したスレッドをクリアできるようにする必要があります。助けていただければ幸いです!

49
Jeff Allen

私はこの問題に自分で対処しました。他のいくつかの回答とは異なり、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();
}

これにより、スレッドが適切にシャットダウンされ、エラーが消えます。

52
Bill

私は同じ問題を抱えており、ジェフが言うように、「アプローチすることを心配しないでください」という方法はありませんでした。

コンテキストが閉じられるとハングしたスレッドを停止するServletContextListenerを実行し、そのようなContextListenerをweb.xmlファイルに登録しました。

スレッドを停止することはそれらに対処するためのエレガントな方法ではないことは既に知っていますが、それ以外の場合、2つまたは3つのデプロイ後にサーバーがクラッシュし続けます(アプリサーバーを再起動することが常に可能とは限りません)。

私が作成したクラスは次のとおりです。

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>
15
Oso

最も侵襲性の低い回避策は、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 JDBCドライバーをwebapp.warのWEB-INF/libディレクトリ内ではなく、Tomcatのlibディレクトリに配置することを前提としています。全体のポイントは、ドライバーbeforeを個別にロードすることですあなたのwebappの。

参照:

13
Stefan L

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対話ではスレッドは実行されません。

11
Lawrence Dol

上記の答えの最良の部分を取り上げ、それらを簡単に拡張可能なクラスに結合しました。これは、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://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();
                        }
                    }
                }
            }
        }
    }

}

これは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の実装が改善されたため、開発者はこの状況に対処する4つの方法があります。

  • デフォルトのTomcat構成が使用され、Connector/J jarがローカルライブラリディレクトリに配置されると、Connector/Jの新しい組み込みアプリケーション検出器は、5秒以内にWebアプリケーションの停止を検出し、AbandonedConnectionCleanupThreadを強制終了します。スレッドが停止できないという不必要な警告も回避されます。 Connector/J jarがグローバルライブラリディレクトリに配置される場合、スレッドはJVMがアンロードされるまで実行されたままになります。

  • Tomcatのコンテキストが属性clearReferencesStopThreads = "true"で構成されている場合、Connector/Jが他のWebアプリケーションと共有されていない限り、Tomcatはアプリケーションの停止時に生成されたスレッドをすべて停止します。 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);
2
Le Zhang

Osoからさらに一歩進んで、上記のコードを2つの点で改善しました。

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

ビルのソリューションは良さそうですが、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)を使用していくつかのテストを行いました。

回避策スレッドダンプを適用する前に、webappを数回再デプロイした後、複数のAbandonedConnectionCleanupThreadインスタンスがリストされました。回避策を適用すると、AbandonedConnectionCleanupThreadインスタンスは1つだけになります。

ただし、アプリを修正し、MySQLドライバーをwebappからTomcat libに移動する必要がありました。それ以外の場合、クラスローダーはTomcatの起動時にcom.mysql.jdbc.NonRegisteringDriverをロードできません。

まだこの問題と戦っているすべての人に役立つことを願っています...

2
wmiki

メモリリークを防ぐため、JDBCドライバは強制的に登録解除されました を参照してください。ビルの回答は、すべてのドライバーインスタンスと、他のWebアプリケーションに属する可能性のあるインスタンスの登録を解除します。ドライバーインスタンスが正しい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()メソッドは静的であるため、そうではありません。

1
Martijn Dirkse