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