1. 程式人生 > >springboot打包成war,由於log4j2 starter使用不當而引發的memory leak排查解決

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.總結

記錄了一次簡單的記憶體洩漏排查和解決過程。

重點在於,解決問題的時候要稍微想一想工作原理大笑