淺談Java中ThreadLocal引發(fā)的內(nèi)存泄漏
預(yù)備知識(引用)
Object o = new Object();
這個(gè)o,我們可以稱之為對象引用,而new Object()我們可以稱之為在內(nèi)存中產(chǎn)生了一個(gè)對象實(shí)例。
當(dāng)寫下 o=null時(shí),只是表示o不再指向堆中object的對象實(shí)例,不代表這個(gè)對象實(shí)例不存在了。
- 強(qiáng)引用: 就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象實(shí)例。
- 軟引用: 是用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象實(shí)例列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類來實(shí)現(xiàn)軟引用。
- 弱引用: 也是用來描述非必需對象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象實(shí)例只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象實(shí)例。在JDK 1.2之后,提供了WeakReference類來實(shí)現(xiàn)弱引用。
- 虛引用: 也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個(gè)對象實(shí)例是否有虛引用的存在,完全不會對其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對象實(shí)例。為一個(gè)對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對象實(shí)例被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在之后,提供了類來實(shí)現(xiàn)虛引用
內(nèi)存泄漏的現(xiàn)象
/** * 類說明:ThreadLocal造成的內(nèi)存泄漏演示 */ public class ThreadLocalOOM { private static final int TASK_LOOP_SIZE = 500; final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); static class LocalVariable { private byte[] a = new byte[1024*1024*5];/*5M大小的數(shù)組*/ } final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { Object o = new Object(); /*5*5=25*/ for (int i = 0; i < TASK_LOOP_SIZE; ++i) { poolExecutor.execute(new Runnable() { public void run() { //localVariable.set(new LocalVariable()); new LocalVariable(); System.out.println("use local varaible"); //localVariable.remove(); } }); Thread.sleep(100); } System.out.println("pool execute over"); } }
首先只簡單的在每個(gè)任務(wù)中new出一個(gè)數(shù)組
可以看到內(nèi)存的實(shí)際使用控制在25M左右:因?yàn)槊總€(gè)任務(wù)中會不斷new出一個(gè)5M的數(shù)組,5*5=25M,這是很合理的。
當(dāng)我們啟用了ThreadLocal以后
內(nèi)存占用最高升至150M,一般情況下穩(wěn)定在90M左右,那么加入一個(gè)ThreadLocal后,內(nèi)存的占用真的會這么多?
于是,我們加入一行代碼:
再執(zhí)行,看看內(nèi)存情況:
可以看見最高峰的內(nèi)存占用也在25M左右,完全和我們不加ThreadLocal表現(xiàn)一樣。
這就充分說明,確實(shí)發(fā)生了內(nèi)存泄漏。
分析
根據(jù)我們前面對ThreadLocal的分析,我們可以知道每個(gè)Thread 維護(hù)一個(gè) ThreadLocalMap,這個(gè)映射表的 key 是 ThreadLocal實(shí)例本身,value 是真正需要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值,它只是作為一個(gè) key 來讓線程從 ThreadLocalMap 獲取 value。仔細(xì)觀察ThreadLocalMap,這個(gè)map是使用 ThreadLocal 的弱引用作為 Key 的,弱引用的對象在 GC 時(shí)會被回收。
因此使用了ThreadLocal后,引用鏈如圖所示
圖中的虛線表示弱引用。
? 這樣,當(dāng)把threadlocal變量置為null以后,沒有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal將會被gc回收。這樣一來,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會一直存在一條強(qiáng)引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊value永遠(yuǎn)不會被訪問到了,所以存在著內(nèi)存泄露。
? 只有當(dāng)前thread結(jié)束以后,current thread就不會存在棧中,強(qiáng)引用斷開,Current Thread、Map value將全部被GC回收。最好的做法是不在需要使用ThreadLocal變量后,都調(diào)用它的remove()方法,清除數(shù)據(jù)。
? 其實(shí)考察ThreadLocal的實(shí)現(xiàn),我們可以看見,無論是get()、set()在某些時(shí)候,調(diào)用了expungeStaleEntry方法用來清除Entry中Key為null的Value,但是這是不及時(shí)的,也不是每次都會執(zhí)行的,所以一些情況下還是會發(fā)生內(nèi)存泄露。只有remove()方法中顯式調(diào)用了expungeStaleEntry方法。
? 從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但是另一個(gè)問題也同樣值得思考:為什么使用弱引用而不是強(qiáng)引用?
下面我們分兩種情況討論:
- ? key 使用強(qiáng)引用:引用ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用,如果沒有手動刪除,ThreadLocal的對象實(shí)例不會被回收,導(dǎo)致Entry內(nèi)存泄漏。
- ? key 使用弱引用:引用的ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal的對象實(shí)例也會被回收。value在下一次ThreadLocalMap調(diào)用set,get,remove都有機(jī)會被回收。
? 比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應(yīng)key,都會導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障。
? 因此,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應(yīng)key就會導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?/p>
為什么ThreadLocalMap的key要設(shè)置為弱引用?
在 ThreadLocalMap 中的set和get方法中,會對 key為null進(jìn)行判斷,如果key為null會把value也置為null。
這樣就算忘記調(diào)用remove方法,對應(yīng)的value在下次調(diào)用get、set、remove方法中的任意一個(gè)都會被清除,從而避免內(nèi)存泄漏(相當(dāng)于多了一層保障,但是如果后續(xù)一直不調(diào)用這些方法,依然存在內(nèi)存泄漏的風(fēng)險(xiǎn),所以最好是及時(shí)remove)。
總結(jié)
? JVM利用設(shè)置ThreadLocalMap的Key為弱引用,來避免內(nèi)存泄露。
JVM利用調(diào)用remove、get、set方法的時(shí)候,回收弱引用。
當(dāng)ThreadLocal存儲很多Key為null的Entry的時(shí)候,而不再去調(diào)用remove、get、set方法,那么將導(dǎo)致內(nèi)存泄漏。
使用線程池+ ThreadLocal 時(shí)要小心,因?yàn)檫@種情況下,線程是一直在不斷的重復(fù)運(yùn)行的,從而也就造成了value可能造成累積的情況。
錯(cuò)誤使用ThreadLocal導(dǎo)致線程不安全
/** * 非安全的ThreadLocal 演示 */ public class ThreadLocalUnsafe implements Runnable { public static ThreadLocal<Number> numberThreadLocal = new ThreadLocal<Number>(); /** * 使用threadLocal的靜態(tài)變量 */ public static Number number = new Number(0); public void run() { //每個(gè)線程計(jì)數(shù)加一 number.setNum(number.getNum() + 1); //將其存儲到ThreadLocal中 numberThreadLocal.set(number); //延時(shí)2ms try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //輸出num值 System.out.println("內(nèi)存地址:"+numberThreadLocal.get() + "," + Thread.currentThread().getName() + "=" + numberThreadLocal.get().getNum()); } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(new ThreadLocalUnsafe()).start(); } } /** * 一個(gè)私有的類 Number */ private static class Number { public Number(int num) { this.num = num; } private int num; public int getNum() { return num; } public void setNum(int num) { this.num = num; } } }
輸出:
內(nèi)存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-2=5
內(nèi)存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-0=5
內(nèi)存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-4=5
內(nèi)存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-1=5
內(nèi)存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-3=5
? 為什么每個(gè)線程都輸出5?難道他們沒有獨(dú)自保存自己的Number副本嗎?為什么其他線程還是能夠修改這個(gè)值?仔細(xì)考察下我們的代碼,我們發(fā)現(xiàn)我們的number對象是靜態(tài)的,所以每個(gè)ThreadLoalMap中保存的其實(shí)同一個(gè)對象的引用,這樣的話,當(dāng)有其他線程對這個(gè)引用指向的對象實(shí)例做修改時(shí),其實(shí)也同時(shí)影響了所有的線程持有的對象引用所指向的同一個(gè)對象實(shí)例。這也就是為什么上面的程序?yàn)槭裁磿敵鲆粯拥慕Y(jié)果:5個(gè)線程中保存的是同一Number對象的引用,在線程睡眠的時(shí)候,其他線程將num變量進(jìn)行了修改,而修改的對象Number的實(shí)例是同一份,因此它們最終輸出的結(jié)果是相同的。
而上面的程序要正常的工作,應(yīng)該去掉number的static 修飾,讓每個(gè)ThreadLoalMap中使用不同的number對象進(jìn)行操作。
總結(jié):ThreadLocal只保證線程隔離,不保證線程安全。
到此這篇關(guān)于淺談Java中ThreadLocal引發(fā)的內(nèi)存泄漏的文章就介紹到這了,更多相關(guān)Java ThreadLocal內(nèi)存泄漏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解RabbitMQ延遲隊(duì)列的基本使用和優(yōu)化
這篇文章主要介紹了詳解RabbitMQ延遲隊(duì)列的基本使用和優(yōu)化,延遲隊(duì)列中的元素都是帶有時(shí)間屬性的。延遲隊(duì)列就是用來存放需要在指定時(shí)間被處理的元素的隊(duì)列,需要的朋友可以參考下2023-05-05java實(shí)現(xiàn)兩個(gè)對象之間傳值及簡單的封裝
這篇文章主要介紹了java實(shí)現(xiàn)兩個(gè)對象之間傳值及簡單的封裝,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Java Volatile應(yīng)用單例模式實(shí)現(xiàn)過程解析
這篇文章主要介紹了Java Volatile應(yīng)用單例模式實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11一篇超詳細(xì)的Spring Boot整合Mybatis文章
大家都知道springboot搭建一個(gè)spring框架只需要秒秒鐘。下面通過實(shí)例代碼給大家介紹一下springboot與mybatis的完美融合,非常不錯(cuò),具有參考借鑒價(jià)值,感興趣的朋友一起看看吧2021-07-07Java 線程池ExecutorService詳解及實(shí)例代碼
這篇文章主要介紹了Java 線程池ExecutorService詳解及實(shí)例代碼的相關(guān)資料,線程池減少在創(chuàng)建和銷毀線程上所花的時(shí)間以及系統(tǒng)資源的開銷.如果不使用線程池,有可能造成系統(tǒng)創(chuàng)建大量線程而導(dǎo)致消耗系統(tǒng)內(nèi)存以及”過度切換“2016-11-11