前言
之前寫了一篇
深入分析 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
洩漏必然是因為它被别的對象強引用了,那麼我們可以嘗試畫出它們的引用關系圖。等等!類加載器的作用到底是啥?為什麼會被強引用?
類的生命周期與類加載器
要解決上面的問題,我們得去研究一下類的生命周期和類加載器的關系。這個問題說起來又是一篇文章,參考我做的筆記
類的生命周期跟我們這個案例相關的主要是類的解除安裝:
在類使用完之後,如果滿足下面的情況,類就會被解除安裝:
- 該類所有的執行個體都已經被回收,也就是 Java 堆中不存在該類的任何執行個體。
- 加載該類的
已經被回收。ClassLoader
- 該類對應的
對象沒有任何地方被引用,沒有在任何地方通過反射通路該類的方法。java.lang.Class
如果以上三個條件全部滿足,JVM 就會在方法區垃圾回收的時候對類進行解除安裝,類的解除安裝過程其實就是在方法區中清空類資訊,Java 類的整個生命周期就結束了。
由Java虛拟機自帶的類加載器所加載的類,在虛拟機的生命周期中,始終不會被解除安裝。Java虛拟機自帶的類加載器包括根類加載器、擴充類加載器和系統類加載器。Java虛拟機本身會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,是以這些Class對象始終是可觸及的。
由使用者自定義的類加載器加載的類是可以被解除安裝的。
注意上面這句話,
WebappClassLoader
如果洩漏了,意味着它加載的類都無法被解除安裝,這就解釋了為什麼上面的代碼會導緻 PermGen
OutOfMemoryException
關鍵點看下面這幅圖

我們可以發現:類加載器對象跟它加載的 Class 對象是雙向關聯的。這意味着,Class 對象可能就是強引用
WebappClassLoader
,導緻它洩漏的元兇。
引用關系圖
了解類加載器與類的生命周期的關系之後,我們可以開始畫引用關系圖了。(圖中的
LeakingServlet.class
與
myThreadLocal
引用畫的不嚴謹,主要是想表達
myThreadLocal
是類變量的意思)
下面,我們根據上面的圖來分析
WebappClassLoader
洩漏的原因。
-
持有LeakingServlet
的static
,導緻MyThreadLocal
的生命周期跟myThreadLocal
類的生命周期一樣長。意味着LeakingServlet
不會被回收,弱引用形同虛設,是以目前線程無法通過myThreadLocal
的防護措施清除ThreadLocalMap
的強引用(見 )。counter
- 強引用鍊:
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();
}
}
提示
歡迎大家批評指正,留言交流。
參考文章
ClassLoader記憶體溢出-從tomcat的reload說起 類加載器記憶體洩露與tomcat自定義加載器 Tomcat源碼解讀系列(四)——Tomcat類加載機制概述