首頁  >  文章  >  Java  >  Java中ThreadLocal記憶體洩漏的程式碼實例分享(圖)

Java中ThreadLocal記憶體洩漏的程式碼實例分享(圖)

黄舟
黄舟原創
2017-03-23 10:47:171819瀏覽

前言

之前寫了一篇深入分析ThreadLocal 記憶體洩漏問題是從理論上分析<span class="wp_keywordlink">ThreadLocal</span>的記憶體洩漏問題,這篇文章我們來分析一下實際的記憶體洩漏案例。分析問題的過程比結果更重要,理論結合實際上才能徹底分析記憶體洩漏的原因。

案例與分析

問題背景

在 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.<a href="http://www.php.cn/java/java-ymp-Lang.html" target="_blank">lang</a>.Class物件沒有任何地方被引用,沒有在任何地方透過反射來存取該類別的方法。

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

由Java虛擬機器自帶的類別載入器所載入的類,在虛擬機器的生命週期中,始終不會被卸載。 Java虛擬機器自帶的類別載入器包括根類別載入器、擴充類別載入器和系統類別載入器。 Java虛擬機本身會始終引用這些類別載入器,而這些類別載入器則會始終引用它們所載入的類別的Class對象,因此這些Class物件始終是可觸及的。

由使用者自訂的類別載入器載入的類別是可以被卸載的。

注意上面這句話,WebappClassLoader如果洩漏了,表示它載入的類別都無法被卸載,這就解釋了為什麼上面的程式碼會導致PermGen OutOfMemoryException

關鍵點看下面這張圖

我們可以發現:類別載入器物件跟它載入的 Class 物件是雙向關聯的。這意味著,Class 物件可能是強引用WebappClassLoader,導致它洩漏的元兇。

引用關係圖

理解類別載入器與類別的生命週期的關係之後,我們可以開始畫引用關係圖了。 (圖中的LeakingServlet.classmyThreadLocal引用畫的不嚴謹,主要是想表達myThreadLocal是類別變數的意思)

Java中ThreadLocal記憶體洩漏的程式碼實例分享(圖)

下面,我们根据上面的图来分析WebappClassLoader泄漏的原因。

  1. LeakingServlet持有staticMyThreadLocal,导致myThreadLocal的生命周期跟LeakingServlet类的生命周期一样长。意味着myThreadLocal不会被回收,弱引用形同虚设,所以当前线程无法通过ThreadLocal<a href="http://www.php.cn/code/8210.html" target="_blank">Map</a>的防护措施清除counter的强引用。

  2. 强引用链:thread -> threadLocalMap -> counter -> MyCounter.class -> WebappClassLocader,导致WebappClassLoader泄漏。

总结

内存泄漏是很难发现的问题,往往由于多方面原因造成。ThreadLocal由于它与线程绑定的生命周期成为了内存泄漏的常客,稍有不慎就酿成大祸。

本文只是对一个特定案例的分析,若能以此举一反三,那便是极好的。最后我留另一个类似的案例供读者分析。

课后题

假设我们有一个定义在 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();
        }
}

提示

Java中ThreadLocal記憶體洩漏的程式碼實例分享(圖)

以上是Java中ThreadLocal記憶體洩漏的程式碼實例分享(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn