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

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

4

9 に答える 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();
}

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

于 2013-05-09T17:33:58.127 に答える
15

私も同じ問題を抱えていましたが、ジェフが言うように、「アプローチについて心配する必要はありません」というのは道のりではありませんでした。

コンテキストが閉じられているときにハングしたスレッドを停止する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>
于 2012-09-16T05:16:35.080 に答える
14

最も侵襲性の低い回避策は、Webアプリケーションのクラスローダーの外部のコードからMySQLJDBCドライバーの初期化を強制することです。

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.xcom.mysql.cj.jdbc.NonRegisteringDriverでは代わりに使用してください

これは、MySQLJDBCドライバーをwebapp.warのWEB-INF/libディレクトリー内ではなく、tomcatのlibディレクトリーに配置することを前提としています。これは、Webアプリケーションのに独立してドライバーをロードすることが重要だからです。

参照:

于 2013-09-26T12:09:41.950 に答える
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インタラクションで実行されません。

于 2013-07-27T00:06:08.123 に答える
6

私は上記の答えの最良の部分を取り、それらを簡単に拡張可能なクラスにまとめました。これは、Osoの最初の提案と、Billのドライバーの改善およびSoftwareMonkeyの反射の改善を組み合わせたものです。(Stephan Lの答えの単純さも気に入りましたが、特に自動スケーリングや別のWebコンテナーへの移行を処理する必要がある場合は、Tomcat環境自体を変更することが適切でない場合があります。)

クラス名、スレッド名、および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();
                        }
                    }
                }
            }
        }
    }

}
于 2014-01-06T22:27:48.547 に答える
2

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

  1. 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
            }
        }
    }
    
  2. スレッドが停止する時間を与えるために、少しの間スリープします。それがなければ、tomcatは文句を言い続けました。

    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        logger.debug(e.getMessage(), e);
    }
    
于 2012-11-13T15:31:56.433 に答える
2

Billのソリューションは良さそうですが、MySQLのバグレポートで直接別のソリューションを見つけました。

[2013年6月5日17:12]ChristopherSchultzこれは、何かが変わるまでのはるかに優れた回避策です。

TomcatのJreMemoryLeakPreventionListener(Tomcat 7ではデフォルトで有効)を有効にし、次の属性を要素に追加します。

classesToInitialize = "com.mysql.jdbc.NonRegisteringDriver"

「classesToInitialize」がすでに設定されている場合は、コンマで区切った既存の値にNonRegisteringDriverを追加するだけです。

そして答え:

[2013年6月8日21:33]MarkoAsplund JreMemoryLeakPreventionListener / classesToInitialize回避策(Tomcat 7.0.39 + MySQL Connector / J 5.1.25)を使用してテストを行いました。

回避策を適用する前に、Webアプリケーションを数回再デプロイした後、複数のAbandonedConnectionCleanupThreadインスタンスがリストされたスレッドダンプ。回避策を適用した後、AbandonedConnectionCleanupThreadインスタンスは1つだけです。

ただし、アプリを変更し、MySQLドライバーをwebappからTomcatlibに移動する必要がありました。 そうしないと、クラスローダーはTomcatの起動時にcom.mysql.jdbc.NonRegisteringDriverをロードできません。

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

于 2014-06-28T22:34:51.120 に答える
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の実装が改善され、開発者がこの状況に対処する方法が4つになりました。

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

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

メモリリークを防ぐために、JDBCドライバは強制的に登録解除されましたを参照してください。Billの回答は、すべてのDriverインスタンスと、他のWebアプリケーションに属する可能性のあるインスタンスの登録を解除します。Driverインスタンスが右に属していることを確認して、Billの回答を拡張しました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()ます。

于 2016-04-27T17:43:12.477 に答える