案例與分析
問題背景
在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 。
關鍵點看下面這幅圖
我們可以發現:類加載器對象跟它加載的Class 對像是雙向關聯的。這意味著,Class 對象可能就是強引用WebappClassLoader ,導致它洩漏的元兇。
引用關係圖
理解類加載器與類的生命週期的關係之後,我們可以開始畫引用關係圖了。 (圖中的LeakingServlet.class與myThreadLocal引用畫的不嚴謹,主要是想表達myThreadLocal是類變量的意思)
下面,我們根據上面的圖來分析WebappClassLoader洩漏的原因。
1、 LeakingServlet持有static的MyThreadLocal ,導致myThreadLocal的生命週期跟LeakingServlet類的生命週期一樣長。意味著myThreadLocal不會被回收,弱引用形同虛設,所以當前線程無法通過ThreadLocalMap的防護措施清除counter的強引用。
2、強引用鏈: thread -> threadLocalMap -> counter -> MyCounter.class -> WebappClassLocader ,導致WebappClassLoader洩漏。
總結
內存洩漏是很難發現的問題,往往由於多方面原因造成。 ThreadLocal由於它與線程綁定的生命週期成為了內存洩漏的常客,稍有不慎就釀成大禍。本文只是對一個特定案例的分析,若能以此舉一反三,那便是極好的。希望本文對大家能有所幫助。