Утечка памяти 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.

Я знаю, что это похоже на другие вопросы (например, этот), но в моем случае ответ "не волнуйся об этом" выиграл ' достаточно, поскольку это вызывает проблемы для меня. У меня есть сервер CI, который регулярно обновляет это приложение, и после 6-10 перезагрузок сервер CI будет зависать, потому что Tomcat не работает.

Мне нужно прояснить эти осиротевшие потоки, чтобы я мог более уверенно запускать свой CI-сервер. Любая помощь будет оценена!

Ответ 1

Я только сам справился с этой проблемой. Вопреки некоторым другим ответам, я не рекомендую выдавать команду 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();
}

Это должно правильно отключить поток, и ошибка должна исчезнуть.

Ответ 2

У меня была одна и та же проблема, и, как говорит Джефф, "не беспокойтесь об этом" не было.

Я сделал ServletContextListener, который останавливает зависающий поток при закрытии контекста, а затем зарегистрировал такой ContextListener в файле web.xml.

Я уже знаю, что остановка потока - не изящный способ справиться с ними, но в противном случае сервер продолжает сбой после двух или трех разворачиваний (не всегда можно перезапустить сервер приложений).

Созданный мной класс:

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>

Ответ 3

Начиная с версии 5.1.23 от MySQL, предоставляется метод для закрытия потока очистки отказанного соединения, 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.

Ответ 4

Наименее инвазивным обходным путем является принудительная инициализация драйвера JDBC MySQL из кода вне загрузчика классов webapp.

В tomcat/conf/server.xml измените (внутри элемента Server):

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />

к

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"
          classesToInitialize="com.mysql.jdbc.NonRegisteringDriver" />

Предполагается, что вы установите драйвер JDBC MySQL в каталог tomcat lib, а не внутри вашего каталога webapp.war WEB-INF/lib, так как все дело в том, чтобы загрузить драйвер до и независимо от вашего webapp.

Литература:

Ответ 5

Я взял лучшие части ответов выше и объединил их в легко расширяемый класс. Это сочетает в себе оригинальное предложение Oso с улучшением драйверов Bill и улучшением отражения Software Monkey. (Мне понравилась простота ответа Stephan L, но иногда изменение самой среды Tomcat не является хорошим вариантом, особенно если вам приходится иметь дело с автомасштабированием или переносом в другой веб-контейнер.)

Вместо прямого обращения к имени класса, имени потока и методу остановки я также инкапсулировал их в частный внутренний класс 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 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 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();
                        }
                    }
                }
            }
        }
    }

}

Ответ 6

Я пошел дальше от 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);
    }
    

Ответ 7

Решение Bill выглядит хорошо, однако я нашел другое решение непосредственно в отчетах об ошибках MySQL:

[5 июня 2013 17:12] Кристофер Шульц Здесь намного лучше обходной путь, пока что-то не изменится.

Включить Tomcat JreMemoryLeakPreventionListener (включен по умолчанию на Tomcat 7) и добавить этот атрибут к элементу:

classesToInitialize = "com.mysql.jdbc.NonRegisteringDriver"

Если для вас уже установлено "classesToInitialize", просто добавьте NonRegisteringDriver в существующее значение, разделенное запятой.

и ответ:

[8 Июн. 2013 21:33] Марко Асплунд Я провел некоторое тестирование с помощью метода обхода JreMemoryLeakPreventionListener/classesToInitialize (Tomcat 7.0.39 + MySQL Connector/J 5.1.25).

Перед применением обходных дампов потоков перечислены несколько экземпляров AbandonedConnectionCleanupThread после повторного развертывания webapp несколько раз. После применения обходного пути существует только один экземпляр AbandonedConnectionCleanupThread.

Мне пришлось модифицировать мое приложение, хотя и переместить драйвер MySQL из webapp в Tomcat lib.В противном случае загрузчик классов не сможет загрузить com.mysql.jdbc.NonRegisteringDriver при запуске Tomcat.

Я надеюсь, что это поможет всем, кто все еще борется с этой проблемой...

Ответ 8

См. Чтобы предотвратить утечку памяти, Драйвер JDBC был принудительно незарегистрирован. Билл отвечает на регистрацию всех экземпляров драйвера, а также экземпляров, которые могут принадлежать другим веб-приложениям. Я продлил ответ Билла с проверкой, что экземпляр драйвера принадлежит правилу ClassLoader.

Вот результирующий код (в отдельном методе, потому что у меня contextDestroyed есть другие вещи):

// See https://stackoverflow.com/questions/25699985/the-web-application-appears-to-have-started-a-thread-named-abandoned-connect
// and
// /questions/26009/to-prevent-a-memory-leak-the-jdbc-driver-has-been-forcibly-unregistered/190250#190250
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(). Может ли он вмешиваться в другие веб-приложения? Надеюсь, что нет, потому что метод AbandonedConnectionCleanupThread.run() не является статическим, но метод AbandonedConnectionCleanupThread.shutdown().

Ответ 9

Кажется, это было исправлено в 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 по умолчанию, а коннектор /J jar помещается в каталог локальной библиотеки, новый встроенный детектор приложения в Connector/J теперь обнаруживает остановку веб-приложения в течение 5 секунд и убивает AbandonedConnectionCleanupThread. Также избегаются любые ненужные предупреждения о невозможности остановить нить. Если Connector/J jar помещается в каталог глобальной библиотеки, поток остается включенным до тех пор, пока JVM не будет выгружен.

  • Когда контекст Tomcat настроен с атрибутом clearReferencesStopThreads = "true", Tomcat собирается остановить все порожденные потоки, когда приложение остановится, если Connector/J не будет использоваться совместно с другими веб-приложениями, и в этом случае Connector/J теперь защищена от неуместной остановки Tomcat; предупреждение о потоке non-stoppable все еще выдается в журнал ошибок Tomcat.

  • Когда ServletContextListener реализуется в каждом веб-приложении, которое вызывает AbandonedConnectionCleanupThread.checkedShutdown() при уничтожении контекста, теперь Connector/J пропускает эту операцию, если драйвер потенциально доступен другим приложениям. В этом случае в журнал ошибок Tomcat не выводится предупреждение о том, что нить, которая перестает быть заблокированной, выдается.

  • Когда вызывается AbandonedConnectionCleanupThread.uncheckedShutdown(), AbandonedConnectionCleanupThread закрывается, даже если Connector/J используется совместно с другими приложениями. Однако впоследствии может быть невозможно перезапустить поток.

Если вы посмотрите на исходный код, они вызвали setDeamon (true) в потоке, поэтому он не будет блокировать выключение.

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