springboot打包成war,由於log4j2 starter使用不當而引發的memory leak排查解決
1.背景
公司有個springboot專案,需要打包成war釋出到tomcat,無意間看了一驗tomcat日誌,發現在shutdown過程中有一些異常資訊,如下:
27-Jun-2018 08:46:42.332 WARNING [mainApp.com-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [Log4j2-TF-7-Scheduled-3] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread: sun.misc.Unsafe.park(Native Method) java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078) java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1093) java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809) java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067) java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127) java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) java.lang.Thread.run(Thread.java:745) 27-Jun-2018 08:46:42.334 WARNING [mainApp.com-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [Log4j2-TF-4-AsyncLoggerConfig-4] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread: sun.misc.Unsafe.park(Native Method) java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078) com.lmax.disruptor.TimeoutBlockingWaitStrategy.waitFor(TimeoutBlockingWaitStrategy.java:38) com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56) com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159) com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125) java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) java.lang.Thread.run(Thread.java:745)
這個日誌描述的還是比較清楚的,可以發現tomcat警告有兩個log4j的執行緒stop失敗了,他說似乎是記憶體洩露。
2.解決
看下專案的依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- log4j --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
這邊是排除了springboot的預設日誌框架,依賴spring-boot-starter-log4j2使用log4j2,乍一看沒啥問題。
有一段很顯眼的英文:
太多了,而且英文不是很好,我藉助一下百度翻譯:
當使用JavaEE Web應用程式中的Log4J或任何其他日誌記錄框架時,必須特別小心。當容器關閉或Web應用程式解除安裝時,對日誌資源進行適當清理(資料庫連線關閉、檔案關閉等)是很重要的。由於Web應用程式中類裝載器的性質,無法通過正常方式清理Log4J資源。當Web應用程式部署和“關閉”Web應用程式未解除安裝時,Log4J必須“啟動”。這取決於應用程式是Servlet 3還是更新的或servlet 2.5 Web應用程式。
在這兩種情況下,您需要將Log4J Web模組新增到部署中,如Maven、Ivy和Gradle工件手冊頁中詳細說明的那樣。
也就是說我們如果是一個web程式,則需要藉助log4j-web這個依賴,這個jar中有一個ServletContainerInitializer的實現類:Log4jServletContainerInitializer.java。
先看下ServletContainerInitializer.java:
public interface ServletContainerInitializer {
/**
* Receives notification during startup of a web application of the classes
* within the web application that matched the criteria defined via the
* {@link javax.servlet.annotation.HandlesTypes} annotation.
*
* @param c The (possibly null) set of classes that met the specified
* criteria
* @param ctx The ServletContext of the web application in which the
* classes were discovered
*
* @throws ServletException If an error occurs
*/
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
這是Servlet3.0的新特性,servlet容器會在啟動時掃描:
META-INF/services/javax.servlet.ServletContainerInitializer這個檔案中指定的類(實現ServletContainerInitializer),這個類將收到容器的onStartup事件。
然後我們看下這個檔案中的內容:
開啟Log4jServletContainerIntializer.java,
public class Log4jServletContainerInitializer implements ServletContainerInitializer {
private static final Logger LOGGER = StatusLogger.getLogger();
public Log4jServletContainerInitializer() {
}
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
if (servletContext.getMajorVersion() > 2 && servletContext.getEffectiveMajorVersion() > 2 && !"true".equalsIgnoreCase(servletContext.getInitParameter("isLog4jAutoInitializationDisabled"))) {
LOGGER.debug("Log4jServletContainerInitializer starting up Log4j in Servlet 3.0+ environment.");
Dynamic filter = servletContext.addFilter("log4jServletFilter", Log4jServletFilter.class);
if (filter == null) {
LOGGER.warn("WARNING: In a Servlet 3.0+ application, you should not define a log4jServletFilter in web.xml. Log4j 2 normally does this for you automatically. Log4j 2 web auto-initialization has been canceled.");
return;
}
Log4jWebLifeCycle initializer = WebLoggerContextUtils.getWebLifeCycle(servletContext);
initializer.start();
initializer.setLoggerContext();
servletContext.addListener(new Log4jServletContextListener());
filter.setAsyncSupported(true);
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, new String[]{"/*"});
}
}
}
這個onStartup中添加了一個監聽器,再看下這個監聽器:
程式碼比較多,就不貼出來了,這個監聽器實現了ServletContextListener介面,然後再contextDestroyed方法中清理了log4j的相關資源,這也就解釋了,網上說引用了log-web依賴後記憶體洩露可以解決。
最後,加入依賴-打包-除錯-檢視日誌,tomcat(我這邊用的是tomcat8)日誌乾淨了,沒有出現memory leak。
3.總結
記錄了一次簡單的記憶體洩漏排查和解決過程。
重點在於,解決問題的時候要稍微想一想工作原理。