天天看點

ThreadLocal 記憶體洩露的執行個體分析

前言

之前寫了一篇

深入分析 ThreadLocal 記憶體洩漏問題

是從理論上分析

ThreadLocal

的記憶體洩漏問題,這一篇文章我們來分析一下實際的記憶體洩漏案例。分析問題的過程比結果更重要,理論結合實際才能徹底分析出記憶體洩漏的原因。

案例與分析

問題背景

在 Tomcat 中,下面的代碼都在 webapp 内,會導緻

WebappClassLoader

洩漏,無法被回收。

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

OutOfMemoryException

解決問題

現在我們來思考一下:為什麼上面的

ThreadLocal

子類會導緻記憶體洩漏?

WebappClassLoader

首先,我們要搞清楚

WebappClassLoader

是什麼鬼?

對于運作在 Java EE容器中的 Web 應用來說,類加載器的實作方式與一般的 Java 應用有所不同。不同的 Web 容器的實作方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器執行個體。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規範中的推薦做法,其目的是使得 Web 應用自己的類的優先級高于 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍之内的。這也是為了保證 Java 核心庫的類型安全。

也就是說

WebappClassLoader

是 Tomcat 加載 webapp 的自定義類加載器,每個 webapp 的類加載器都是不一樣的,這是為了隔離不同應用加載的類。

那麼

WebappClassLoader

的特性跟記憶體洩漏有什麼關系呢?目前還看不出來,但是它的一個很重要的特點值得我們注意:每個 webapp 都會自己的

WebappClassLoader

,這跟 Java 核心的類加載器不一樣。

我們知道:導緻

WebappClassLoader

洩漏必然是因為它被别的對象強引用了,那麼我們可以嘗試畫出它們的引用關系圖。等等!類加載器的作用到底是啥?為什麼會被強引用?

類的生命周期與類加載器

要解決上面的問題,我們得去研究一下類的生命周期和類加載器的關系。這個問題說起來又是一篇文章,參考我做的筆記

類的生命周期

跟我們這個案例相關的主要是類的解除安裝:

在類使用完之後,如果滿足下面的情況,類就會被解除安裝:

  1. 該類所有的執行個體都已經被回收,也就是 Java 堆中不存在該類的任何執行個體。
  2. 加載該類的

    ClassLoader

    已經被回收。
  3. 該類對應的

    java.lang.Class

    對象沒有任何地方被引用,沒有在任何地方通過反射通路該類的方法。

如果以上三個條件全部滿足,JVM 就會在方法區垃圾回收的時候對類進行解除安裝,類的解除安裝過程其實就是在方法區中清空類資訊,Java 類的整個生命周期就結束了。

由Java虛拟機自帶的類加載器所加載的類,在虛拟機的生命周期中,始終不會被解除安裝。Java虛拟機自帶的類加載器包括根類加載器、擴充類加載器和系統類加載器。Java虛拟機本身會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,是以這些Class對象始終是可觸及的。

由使用者自定義的類加載器加載的類是可以被解除安裝的。

注意上面這句話,

WebappClassLoader

如果洩漏了,意味着它加載的類都無法被解除安裝,這就解釋了為什麼上面的代碼會導緻 PermGen

OutOfMemoryException

關鍵點看下面這幅圖

ThreadLocal 記憶體洩露的執行個體分析

我們可以發現:類加載器對象跟它加載的 Class 對象是雙向關聯的。這意味着,Class 對象可能就是強引用

WebappClassLoader

,導緻它洩漏的元兇。

引用關系圖

了解類加載器與類的生命周期的關系之後,我們可以開始畫引用關系圖了。(圖中的

LeakingServlet.class

myThreadLocal

引用畫的不嚴謹,主要是想表達

myThreadLocal

是類變量的意思)

ThreadLocal 記憶體洩露的執行個體分析

下面,我們根據上面的圖來分析

WebappClassLoader

洩漏的原因。

  1. LeakingServlet

    持有

    static

    MyThreadLocal

    ,導緻

    myThreadLocal

    的生命周期跟

    LeakingServlet

    類的生命周期一樣長。意味着

    myThreadLocal

    不會被回收,弱引用形同虛設,是以目前線程無法通過

    ThreadLocalMap

    的防護措施清除

    counter

    的強引用(見 )。
  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();
        }
}
           

提示

ThreadLocal 記憶體洩露的執行個體分析

歡迎大家批評指正,留言交流。

參考文章

ClassLoader記憶體溢出-從tomcat的reload說起 類加載器記憶體洩露與tomcat自定義加載器 Tomcat源碼解讀系列(四)——Tomcat類加載機制概述