實(shí)例詳解Java中ThreadLocal內(nèi)存泄露
案例與分析
問(wèn)題背景
在 Tomcat 中,下面的代碼都在 webapp 內(nèi),會(huì)導(dǎo)致WebappClassLoader泄漏,無(wú)法被回收。
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被調(diào)用過(guò)一次,且執(zhí)行它的線(xiàn)程沒(méi)有停止,就會(huì)導(dǎo)致WebappClassLoader泄漏。每次你 reload 一下應(yīng)用,就會(huì)多一份WebappClassLoader實(shí)例,最后導(dǎo)致 PermGen OutOfMemoryException。
解決問(wèn)題
現(xiàn)在我們來(lái)思考一下:為什么上面的ThreadLocal子類(lèi)會(huì)導(dǎo)致內(nèi)存泄漏?
WebappClassLoader
首先,我們要搞清楚WebappClassLoader是什么鬼?
對(duì)于運(yùn)行在 Java EE容器中的 Web 應(yīng)用來(lái)說(shuō),類(lèi)加載器的實(shí)現(xiàn)方式與一般的 Java 應(yīng)用有所不同。不同的 Web 容器的實(shí)現(xiàn)方式也會(huì)有所不同。以 Apache Tomcat 來(lái)說(shuō),每個(gè) Web 應(yīng)用都有一個(gè)對(duì)應(yīng)的類(lèi)加載器實(shí)例。該類(lèi)加載器也使用代理模式,所不同的是它是首先嘗試去加載某個(gè)類(lèi),如果找不到再代理給父類(lèi)加載器。這與一般類(lèi)加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法,其目的是使得 Web 應(yīng)用自己的類(lèi)的優(yōu)先級(jí)高于 Web 容器提供的類(lèi)。這種代理模式的一個(gè)例外是:Java 核心庫(kù)的類(lèi)是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫(kù)的類(lèi)型安全。
也就是說(shuō)WebappClassLoader是 Tomcat 加載 webapp 的自定義類(lèi)加載器,每個(gè) webapp 的類(lèi)加載器都是不一樣的,這是為了隔離不同應(yīng)用加載的類(lèi)。
那么WebappClassLoader的特性跟內(nèi)存泄漏有什么關(guān)系呢?目前還看不出來(lái),但是它的一個(gè)很重要的特點(diǎn)值得我們注意:每個(gè) webapp 都會(huì)自己的WebappClassLoader,這跟 Java 核心的類(lèi)加載器不一樣。
我們知道:導(dǎo)致WebappClassLoader泄漏必然是因?yàn)樗粍e的對(duì)象強(qiáng)引用了,那么我們可以嘗試畫(huà)出它們的引用關(guān)系圖。等等!類(lèi)加載器的作用到底是啥?為什么會(huì)被強(qiáng)引用?
類(lèi)的生命周期與類(lèi)加載器
要解決上面的問(wèn)題,我們得去研究一下類(lèi)的生命周期和類(lèi)加載器的關(guān)系。
跟我們這個(gè)案例相關(guān)的主要是類(lèi)的卸載:
在類(lèi)使用完之后,如果滿(mǎn)足下面的情況,類(lèi)就會(huì)被卸載:
1、該類(lèi)所有的實(shí)例都已經(jīng)被回收,也就是 Java 堆中不存在該類(lèi)的任何實(shí)例。
2、加載該類(lèi)的ClassLoader已經(jīng)被回收。
3、該類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有任何地方被引用,沒(méi)有在任何地方通過(guò)反射訪(fǎng)問(wèn)該類(lèi)的方法。
如果以上三個(gè)條件全部滿(mǎn)足,JVM 就會(huì)在方法區(qū)垃圾回收的時(shí)候?qū)︻?lèi)進(jìn)行卸載,類(lèi)的卸載過(guò)程其實(shí)就是在方法區(qū)中清空類(lèi)信息,Java 類(lèi)的整個(gè)生命周期就結(jié)束了。
由Java虛擬機(jī)自帶的類(lèi)加載器所加載的類(lèi),在虛擬機(jī)的生命周期中,始終不會(huì)被卸載。Java虛擬機(jī)自帶的類(lèi)加載器包括根類(lèi)加載器、擴(kuò)展類(lèi)加載器和系統(tǒng)類(lèi)加載器。Java虛擬機(jī)本身會(huì)始終引用這些類(lèi)加載器,而這些類(lèi)加載器則會(huì)始終引用它們所加載的類(lèi)的Class對(duì)象,因此這些Class對(duì)象始終是可觸及的。
由用戶(hù)自定義的類(lèi)加載器加載的類(lèi)是可以被卸載的。
注意上面這句話(huà),WebappClassLoader如果泄漏了,意味著它加載的類(lèi)都無(wú)法被卸載,這就解釋了為什么上面的代碼會(huì)導(dǎo)致 PermGen OutOfMemoryException。
關(guān)鍵點(diǎn)看下面這幅圖

我們可以發(fā)現(xiàn):類(lèi)加載器對(duì)象跟它加載的 Class 對(duì)象是雙向關(guān)聯(lián)的。這意味著,Class 對(duì)象可能就是強(qiáng)引用WebappClassLoader,導(dǎo)致它泄漏的元兇。
引用關(guān)系圖
理解類(lèi)加載器與類(lèi)的生命周期的關(guān)系之后,我們可以開(kāi)始畫(huà)引用關(guān)系圖了。(圖中的LeakingServlet.class與myThreadLocal引用畫(huà)的不嚴(yán)謹(jǐn),主要是想表達(dá)myThreadLocal是類(lèi)變量的意思)

下面,我們根據(jù)上面的圖來(lái)分析WebappClassLoader泄漏的原因。
1、LeakingServlet持有static的MyThreadLocal,導(dǎo)致myThreadLocal的生命周期跟LeakingServlet類(lèi)的生命周期一樣長(zhǎng)。意味著myThreadLocal不會(huì)被回收,弱引用形同虛設(shè),所以當(dāng)前線(xiàn)程無(wú)法通過(guò)ThreadLocalMap的防護(hù)措施清除counter的強(qiáng)引用。
2、強(qiáng)引用鏈:thread -> threadLocalMap -> counter -> MyCounter.class -> WebappClassLocader,導(dǎo)致WebappClassLoader泄漏。
總結(jié)
內(nèi)存泄漏是很難發(fā)現(xiàn)的問(wèn)題,往往由于多方面原因造成。ThreadLocal由于它與線(xiàn)程綁定的生命周期成為了內(nèi)存泄漏的???,稍有不慎就釀成大禍。本文只是對(duì)一個(gè)特定案例的分析,若能以此舉一反三,那便是極好的。希望本文對(duì)大家能有所幫助。
相關(guān)文章
Java程序執(zhí)行過(guò)程及內(nèi)存機(jī)制詳解
本講將介紹Java代碼是如何一步步運(yùn)行起來(lái)的,還會(huì)介紹Java程序所占用的內(nèi)存是被如何管理的:堆、棧和方法區(qū)都各自負(fù)責(zé)存儲(chǔ)哪些內(nèi)容,感興趣的朋友跟隨小編一起看看吧2020-12-12
Java時(shí)間轉(zhuǎn)換成unix時(shí)間戳的方法
這篇文章主要為大家詳細(xì)介紹了Java時(shí)間轉(zhuǎn)換成unix時(shí)間戳的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12
Spring Boot 捕捉全局異常 統(tǒng)一返回值的問(wèn)題
這篇文章主要介紹了Spring Boot 捕捉全局異常 統(tǒng)一返回值,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06
MybatisPlus?LambdaQueryWrapper使用int默認(rèn)值的坑及解決
這篇文章主要介紹了MybatisPlus?LambdaQueryWrapper使用int默認(rèn)值的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。2022-01-01
IDEA+Maven創(chuàng)建Spring項(xiàng)目的實(shí)現(xiàn)步驟
這篇文章主要介紹了IDEA+Maven創(chuàng)建Spring項(xiàng)目的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Spring常用注解及http數(shù)據(jù)轉(zhuǎn)換教程
這篇文章主要為大家介紹了Spring常用注解及http數(shù)據(jù)轉(zhuǎn)換原理以及接收復(fù)雜嵌套對(duì)象參數(shù)與Http數(shù)據(jù)轉(zhuǎn)換的原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03

