陜西網(wǎng)站建設(shè)公司哪有網(wǎng)絡(luò)推廣都是收費(fèi)
預(yù)備知識(shí)(引用)
Object o = new Object();
這個(gè)o,我們可以稱之為對(duì)象引用,而new Object()我們可以稱之為在內(nèi)存中產(chǎn)生了一個(gè)對(duì)象實(shí)例。
當(dāng)寫下?o=null時(shí),只是表示o不再指向堆中object的對(duì)象實(shí)例,不代表這個(gè)對(duì)象實(shí)例不存在了。
-
強(qiáng)引用:?就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象實(shí)例。
-
軟引用:?是用來(lái)描述一些還有用但并非必需的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象實(shí)例列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類來(lái)實(shí)現(xiàn)軟引用。
-
弱引用:?也是用來(lái)描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象實(shí)例只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象實(shí)例。在JDK 1.2之后,提供了WeakReference類來(lái)實(shí)現(xiàn)弱引用。
-
虛引用:?也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象實(shí)例是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象實(shí)例被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在之后,提供了類來(lái)實(shí)現(xiàn)虛引用
內(nèi)存泄漏的現(xiàn)象
/*** 類說(shuō)明: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");}}
首先只簡(jiǎn)單的在每個(gè)任務(wù)中new出一個(gè)數(shù)組
?可以看到內(nèi)存的實(shí)際使用控制在25M左右:因?yàn)槊總€(gè)任務(wù)中會(huì)不斷new出一個(gè)5M的數(shù)組,5*5=25M,這是很合理的。
當(dāng)我們啟用了ThreadLocal以后
?
內(nèi)存占用最高升至150M,一般情況下穩(wěn)定在90M左右,那么加入一個(gè)ThreadLocal后,內(nèi)存的占用真的會(huì)這么多?
于是,我們加入一行代碼:
?再執(zhí)行,看看內(nèi)存情況:
可以看見(jiàn)最高峰的內(nèi)存占用也在25M左右,完全和我們不加ThreadLocal表現(xiàn)一樣。
這就充分說(shuō)明,確實(shí)發(fā)生了內(nèi)存泄漏。
分析
根據(jù)我們前面對(duì)ThreadLocal的分析,我們可以知道每個(gè)Thread 維護(hù)一個(gè) ThreadLocalMap,這個(gè)映射表的 key 是 ThreadLocal實(shí)例本身,value 是真正需要存儲(chǔ)的 Object,也就是說(shuō) ThreadLocal 本身并不存儲(chǔ)值,它只是作為一個(gè) key 來(lái)讓線程從 ThreadLocalMap 獲取 value。仔細(xì)觀察ThreadLocalMap,這個(gè)map是使用 ThreadLocal 的弱引用作為 Key 的,弱引用的對(duì)象在 GC 時(shí)會(huì)被回收。
因此使用了ThreadLocal后,引用鏈如圖所示
圖中的虛線表示弱引用。
? 這樣,當(dāng)把threadlocal變量置為null以后,沒(méi)有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal將會(huì)被gc回收。這樣一來(lái),ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊value永遠(yuǎn)不會(huì)被訪問(wèn)到了,所以存在著內(nèi)存泄露。
? 只有當(dāng)前thread結(jié)束以后,current thread就不會(huì)存在棧中,強(qiáng)引用斷開,Current Thread、Map value將全部被GC回收。最好的做法是不在需要使用ThreadLocal變量后,都調(diào)用它的remove()方法,清除數(shù)據(jù)。
? 其實(shí)考察ThreadLocal的實(shí)現(xiàn),我們可以看見(jiàn),無(wú)論是get()、set()在某些時(shí)候,調(diào)用了expungeStaleEntry方法用來(lái)清除Entry中Key為null的Value,但是這是不及時(shí)的,也不是每次都會(huì)執(zhí)行的,所以一些情況下還是會(huì)發(fā)生內(nèi)存泄露。只有remove()方法中顯式調(diào)用了expungeStaleEntry方法。
? 從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但是另一個(gè)問(wèn)題也同樣值得思考:為什么使用弱引用而不是強(qiáng)引用?
下面我們分兩種情況討論:
??key 使用強(qiáng)引用:引用ThreadLocal的對(duì)象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用,如果沒(méi)有手動(dòng)刪除,ThreadLocal的對(duì)象實(shí)例不會(huì)被回收,導(dǎo)致Entry內(nèi)存泄漏。
??key 使用弱引用:引用的ThreadLocal的對(duì)象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒(méi)有手動(dòng)刪除,ThreadLocal的對(duì)象實(shí)例也會(huì)被回收。value在下一次ThreadLocalMap調(diào)用set,get,remove都有機(jī)會(huì)被回收。
? 比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果都沒(méi)有手動(dòng)刪除對(duì)應(yīng)key,都會(huì)導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障。
? 因此,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果沒(méi)有手動(dòng)刪除對(duì)應(yīng)key就會(huì)導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?/p>
為什么ThreadLocalMap的key要設(shè)置為弱引用?
在 ThreadLocalMap 中的set和get方法中,會(huì)對(duì) key為null進(jìn)行判斷,如果key為null會(huì)把value也置為null。
這樣就算忘記調(diào)用remove方法,對(duì)應(yīng)的value在下次調(diào)用get、set、remove方法中的任意一個(gè)都會(huì)被清除,從而避免內(nèi)存泄漏(相當(dāng)于多了一層保障,但是如果后續(xù)一直不調(diào)用這些方法,依然存在內(nèi)存泄漏的風(fēng)險(xiǎn),所以最好是及時(shí)remove)。
總結(jié)
? JVM利用設(shè)置ThreadLocalMap的Key為弱引用,來(lái)避免內(nèi)存泄露。
JVM利用調(diào)用remove、get、set方法的時(shí)候,回收弱引用。
當(dāng)ThreadLocal存儲(chǔ)很多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);//將其存儲(chǔ)到ThreadLocal中numberThreadLocal.set(number);//延時(shí)2mstry {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?難道他們沒(méi)有獨(dú)自保存自己的Number副本嗎?為什么其他線程還是能夠修改這個(gè)值?仔細(xì)考察下我們的代碼,我們發(fā)現(xiàn)我們的number對(duì)象是靜態(tài)的,所以每個(gè)ThreadLoalMap中保存的其實(shí)同一個(gè)對(duì)象的引用,這樣的話,當(dāng)有其他線程對(duì)這個(gè)引用指向的對(duì)象實(shí)例做修改時(shí),其實(shí)也同時(shí)影響了所有的線程持有的對(duì)象引用所指向的同一個(gè)對(duì)象實(shí)例。這也就是為什么上面的程序?yàn)槭裁磿?huì)輸出一樣的結(jié)果:5個(gè)線程中保存的是同一Number對(duì)象的引用,在線程睡眠的時(shí)候,其他線程將num變量進(jìn)行了修改,而修改的對(duì)象Number的實(shí)例是同一份,因此它們最終輸出的結(jié)果是相同的。
而上面的程序要正常的工作,應(yīng)該去掉number的static 修飾,讓每個(gè)ThreadLoalMap中使用不同的number對(duì)象進(jìn)行操作。
總結(jié):ThreadLocal只保證線程隔離,不保證線程安全。