ThreadLocal 內存泄露的實例分析

前言

以前寫了一篇深刻分析 ThreadLocal 內存泄漏問題是從理論上分析ThreadLocal的內存泄漏問題,這一篇文章咱們來分析一下實際的內存泄漏案例。分析問題的過程比結果更重要,理論結合實際才能完全分析出內存泄漏的緣由。html

案例與分析

問題背景

在 Tomcat 中,下面的代碼都在 webapp 內,會致使WebappClassLoader泄漏,沒法被回收。java

public class MyCounter {
    private int count = 0;
    public void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

public class MyThreadLocal extends ThreadLocal<MyCounter> {
}

public class LeakingServlet extends HttpServlet {
    private static MyThreadLocal myThreadLocal = new MyThreadLocal();
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws ServletException, IOException {
        MyCounter counter = myThreadLocal.get();
        if (counter == null) {
            counter = new MyCounter();
            myThreadLocal.set(counter);
        }
        response.getWriter().println(
                "The current thread served this servlet " + counter.getCount()
                        + " times");
        counter.increment();
    }
}

上面的代碼中,只要LeakingServlet被調用過一次,且執行它的線程沒有中止,就會致使WebappClassLoader泄漏。每次你 reload 一下應用,就會多一份WebappClassLoader實例,最後致使 PermGen OutOfMemoryExceptionweb

解決問題

如今咱們來思考一下:爲何上面的ThreadLocal子類會致使內存泄漏?apache

WebappClassLoader

首先,咱們要搞清楚WebappClassLoader是什麼鬼?tomcat

對於運行在 Java EE容器中的 Web 應用來講,類加載器的實現方式與通常的 Java 應用有所不一樣。不一樣的 Web 容器的實現方式也會有所不一樣。以 Apache Tomcat 來講,每一個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不一樣的是它是首先嚐試去加載某個類,若是找不到再代理給父類加載器。這與通常類加載器的順序是相反的。這是 Java Servlet 規範中的推薦作法,其目的是使得 Web 應用本身的類的優先級高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍以內的。這也是爲了保證 Java 核心庫的類型安全。安全

也就是說WebappClassLoader是 Tomcat 加載 webapp 的自定義類加載器,每一個 webapp 的類加載器都是不同的,這是爲了隔離不一樣應用加載的類。app

那麼WebappClassLoader的特性跟內存泄漏有什麼關係呢?目前還看不出來,可是它的一個很重要的特色值得咱們注意:每一個 webapp 都會本身的WebappClassLoader,這跟 Java 核心的類加載器不同。webapp

咱們知道:致使WebappClassLoader泄漏必然是由於它被別的對象強引用了,那麼咱們能夠嘗試畫出它們的引用關係圖。等等!類加載器的做用究竟是啥?爲何會被強引用?this

類的生命週期與類加載器

要解決上面的問題,咱們得去研究一下類的生命週期和類加載器的關係。這個問題提及來又是一篇文章,參考我作的筆記類的生命週期spa

跟咱們這個案例相關的主要是類的卸載:

在類使用完以後,若是知足下面的狀況,類就會被卸載:

  1. 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  2. 加載該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class對象沒有任何地方被引用,沒有在任何地方經過反射訪問該類的方法。

若是以上三個條件所有知足,JVM 就會在方法區垃圾回收的時候對類進行卸載,類的卸載過程其實就是在方法區中清空類信息,Java 類的整個生命週期就結束了。

由Java虛擬機自帶的類加載器所加載的類,在虛擬機的生命週期中,始終不會被卸載。Java虛擬機自帶的類加載器包括根類加載器、擴展類加載器和系統類加載器。Java虛擬機自己會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,所以這些Class對象始終是可觸及的。

由用戶自定義的類加載器加載的類是能夠被卸載的。

注意上面這句話,WebappClassLoader若是泄漏了,意味着它加載的類都沒法被卸載,這就解釋了爲何上面的代碼會致使 PermGen OutOfMemoryException

關鍵點看下面這幅圖

咱們能夠發現:類加載器對象跟它加載的 Class 對象是雙向關聯的。這意味着,Class 對象可能就是強引用WebappClassLoader,致使它泄漏的元兇。

引用關係圖

理解類加載器與類的生命週期的關係以後,咱們能夠開始畫引用關係圖了。(圖中的LeakingServlet.classmyThreadLocal引用畫的不嚴謹,主要是想表達myThreadLocal是類變量的意思)
leak_1leak_1

下面,咱們根據上面的圖來分析WebappClassLoader泄漏的緣由。

  1. LeakingServlet持有staticMyThreadLocal,致使myThreadLocal的生命週期跟LeakingServlet類的生命週期同樣長。意味着myThreadLocal不會被回收,弱引用形同虛設,因此當前線程沒法經過ThreadLocalMap的防禦措施清除counter的強引用(見深刻分析 ThreadLocal 內存泄漏問題)。
  2. 強引用鏈:thread -> threadLocalMap -> counter -> MyCounter.class -> WebappClassLocader,致使WebappClassLoader泄漏。

總結

內存泄漏是很難發現的問題,每每因爲多方面緣由形成。ThreadLocal因爲它與線程綁定的生命週期成爲了內存泄漏的常客,稍有不慎就釀成大禍。

本文只是對一個特定案例的分析,若能以此觸類旁通,那即是極好的。最後我留另外一個相似的案例供讀者分析。

本文的案例來自於 Tomcat 的 Wiki MemoryLeakProtection

課後題

假設咱們有一個定義在 Tomcat Common Classpath 下的類(例如說在 tomcat/lib 目錄下)

public class ThreadScopedHolder {
    private final static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>();
    public static void saveInHolder(Object o) {
        threadLocal.set(o);
    }
    public static Object getFromHolder() {
        return threadLocal.get();
    }
}

兩個在 webapp 的類:

public class MyCounter {
    private int count = 0;
    public void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

public class LeakingServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws ServletException, IOException {
        MyCounter counter = (MyCounter) ThreadScopedHolder.getFromHolder();
        if (counter == null) {
            counter = new MyCounter();
            ThreadScopedHolder.saveInHolder(counter);
        }
        response.getWriter().println(
                "The current thread served this servlet " + counter.getCount()
                        + " times");
        counter.increment();
    }
}

提示

leak_2leak_2

歡迎你們批評指正,留言交流。

相關文章
相關標籤/搜索