[英]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" />
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 更進一步,在兩點改進了上面的代碼:
將終結器線程添加到需要終止檢查中:
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); }
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.