Java 中 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
洩漏必然是因為它被別的物件強引用了,那麼我們可以嘗試畫出它們的引用關係圖。等等!類載入器的作用到底是啥?為什麼會被強引用?
類的生命週期與類載入器
要解決上面的問題,我們得去研究一下類的生命週期和類載入器的關係。
跟我們這個案例相關的主要是類的解除安裝:
在類使用完之後,如果滿足下面的情況,類就會被解除安裝:
- 該類所有的例項都已經被回收,也就是 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 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原始碼解讀和記憶體洩露分析thread原始碼記憶體洩露
- Lowmemorykiller記憶體洩露分析記憶體洩露
- Java面試題:細數ThreadLocal大坑,記憶體洩露本可避免Java面試題thread記憶體洩露
- java中如何檢視記憶體洩露Java記憶體洩露
- 使用 mtrace 分析 “記憶體洩露”記憶體洩露
- ThreadLocal原始碼解析,記憶體洩露以及傳遞性thread原始碼記憶體洩露
- Linux記憶體洩露案例分析和記憶體管理分享Linux記憶體洩露
- JAVA記憶體洩露的原因及解決Java記憶體洩露
- Spring Boot heapdump洩露記憶體分析方法Spring Boot記憶體
- SHBrowseForFolder 記憶體洩露記憶體洩露
- 分析ThreadLocal的弱引用與記憶體洩漏問題thread記憶體
- ThreadLocal記憶體洩漏問題thread記憶體
- ThreadLocal真會記憶體洩漏?thread記憶體
- ThreadLocal原理用法詳解ThreadLocal記憶體洩漏thread記憶體
- 記憶體溢位和記憶體洩露記憶體溢位記憶體洩露
- 一個 Vue 頁面的記憶體洩露分析Vue記憶體洩露
- 一個Vue頁面的記憶體洩露分析Vue記憶體洩露
- 小題大做 | Handler記憶體洩露全面分析記憶體洩露
- 面試:為了進阿里,死磕了ThreadLocal記憶體洩露原因面試阿里thread記憶體洩露
- 經驗之談:記憶體洩露的原因以及分析記憶體洩露
- ThreadLocal記憶體洩漏怎麼回事thread記憶體
- ArkTS 的記憶體快照與記憶體洩露除錯記憶體洩露除錯
- 利用dotnet-dump分析docker容器記憶體洩露Docker記憶體洩露
- JVM中java例項物件在記憶體中的佈局JVMJava物件記憶體
- 記一次 .NET 某工控軟體 記憶體洩露分析記憶體洩露
- 線上問題排查例項分析|關於Redis記憶體洩漏Redis記憶體
- 線上問題排查例項分析|關於 Redis 記憶體洩漏Redis記憶體
- 實戰Go記憶體洩露Go記憶體洩露
- Android 記憶體洩露詳解Android記憶體洩露
- Android中使用Handler造成記憶體洩露的分析和解決Android記憶體洩露
- android Handler導致的記憶體洩露Android記憶體洩露
- 例項解析網路程式設計中的另類記憶體洩漏程式設計記憶體
- 使用Windbg快速分析應用記憶體洩露問題記憶體洩露
- 記一次尷尬的Java應用記憶體洩露排查Java記憶體洩露
- nodejs爬蟲記憶體洩露排查NodeJS爬蟲記憶體洩露
- Pprof定位Go程式記憶體洩露Go記憶體洩露
- Java應用程式中的記憶體洩漏及記憶體管理Java記憶體
- 簡單的記憶體“洩露”和“溢位”記憶體
- Python實現記憶體洩露排查的示例Python記憶體洩露