聊聊Java中引用類(lèi)型-引用類(lèi)型應(yīng)用與內(nèi)存泄漏
本文將接著上一篇文章內(nèi)容,聊聊Java中引用使用以及可能產(chǎn)生的內(nèi)存泄漏。
Java程序員是幸福的,不用過(guò)多考慮內(nèi)存申請(qǐng)和釋放,Jvm在Java與C++之間構(gòu)建一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的高墻,是的Java程序員能全身心投入到實(shí)際開(kāi)發(fā)當(dāng)中,是否會(huì)有墻外面人想進(jìn)去,墻里面的人卻想出來(lái)呢?
內(nèi)存溢出和內(nèi)存泄漏:
內(nèi)存溢出:俗稱(chēng)OOM,指JVM無(wú)法申請(qǐng)到足夠內(nèi)存空間或者GC失敗,而拋出的Error,OOM造成的后果十分嚴(yán)重,使得應(yīng)用無(wú)法對(duì)外提供服務(wù)。
內(nèi)存泄漏:部分內(nèi)存已經(jīng)沒(méi)有用了,但是卻沒(méi)有被回收,對(duì)于Java而言,就是GC無(wú)法回收這部分本應(yīng)該被回收的內(nèi)存。
Obeject 的 finalize
Object 的finalize方法,當(dāng)對(duì)象即將被回收時(shí),可以被執(zhí)行finalize方法,但是并不能依賴(lài)這個(gè)方法來(lái)清除,否則將造成內(nèi)存泄漏。例如當(dāng)進(jìn)行socket編程時(shí),需要在finally塊中執(zhí)行close方法,從而釋放資源,如果忘記釋放了,則可能會(huì)造成內(nèi)存泄漏,例如在 重寫(xiě)了finalize方法:
/*** Cleans up if the user forgets to close it.*/protectedvoid finalize()throwsIOException{close();}
方法注釋也很簡(jiǎn)潔明了,所以在釋放這一類(lèi)工具類(lèi)時(shí),一定要手動(dòng)執(zhí)行close方法,而不是交給finalize去替我們釋放,這樣容易引發(fā)內(nèi)存泄漏。
ThreadLocal
ThreadLocal很常見(jiàn),原理不難理解,在Thread類(lèi)中有兩個(gè)ThreadLocalMap變量,維護(hù)著多個(gè)本地線程變量:Thread:
/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals =null;/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals =null;
ThreadLocalMap結(jié)構(gòu):
privatestaticfinalint INITIAL_CAPACITY =16;privateEntry[] table;// 存放元素的數(shù)組privateint size =0;// 大小privateint threshold;// Default to 0
其Entry為一個(gè)WeakReference子類(lèi),reference為對(duì)應(yīng)的ThreadLocal 實(shí)例,即如果沒(méi)有其他引用,就會(huì)被回收:
staticclassEntryextendsWeakReference<ThreadLocal>>{/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal> k,Object v){super(k);value = v;}}
Entry 由于沒(méi)有使用引用隊(duì)列,故一旦沒(méi)有引用,則會(huì)直接變?yōu)閕nactive狀態(tài),從而被gc回收。但是另一方面,ThreadLocalMap 中 Entry[] 為一個(gè)引用,所以事實(shí)上,就算ThreadLocal沒(méi)有其他地方使用,也會(huì)被Thread引用,所以只要Thread不銷(xiāo)毀,Entry 并不會(huì)因WeakReference特性而銷(xiāo)毀。另一方面,由于Entry 中key(ThreadLocal)是弱引用類(lèi)型,所以一旦ThreadLocal沒(méi)有被引用,那么ThreadLocal將會(huì)在下次gc被回收,造成的效果為:
Entry 不為null。
Entry實(shí)例的get方法獲取為null,因?yàn)門(mén)hreadLocal已經(jīng)被回收了,但是value仍然存在,這就造成了泄漏。
在ThreadLocalMap的set操作過(guò)程,雖然在檢查到上述第二種情況,并且會(huì)嘗試將數(shù)組內(nèi)所有滿(mǎn)足該情況的元素節(jié)點(diǎn)的value都設(shè)置為null。這也是為啥追求極致性能的netty會(huì)自己造一個(gè)FastThreadLocal來(lái)取代TheadLocal:https://blog.csdn.net/anLA_/article/details/110777608所以在使用ThreadLocal時(shí),在用完時(shí),一定要執(zhí)行remove方法,清除對(duì)應(yīng)引用。
Netty中內(nèi)存泄漏檢測(cè)
netty中通過(guò)BufAllocator使用ByteBuf時(shí),都會(huì)包裝一層校驗(yàn)內(nèi)存泄漏的邏輯:
protectedstaticByteBuf toLeakAwareBuffer(ByteBuf buf){ResourceLeakTracker<ByteBuf> leak;switch(ResourceLeakDetector.getLevel()){case SIMPLE:leak =AbstractByteBuf.leakDetector.track(buf);if(leak !=null){buf =newSimpleLeakAwareByteBuf(buf, leak);}break;case ADVANCED:case PARANOID:leak =AbstractByteBuf.leakDetector.track(buf);if(leak !=null){buf =newAdvancedLeakAwareByteBuf(buf, leak);}break;default:break;}return buf;}
上述會(huì)將ByteBuf封裝一層 DefaultResourceLeak,而 DefaultResourceLeak 則是一個(gè)WeakReference對(duì)象:
DefaultResourceLeak(Object referent,ReferenceQueue<Object> refQueue,Set<DefaultResourceLeak>> allLeaks){super(referent, refQueue);assert referent !=null;// Store the hash of the tracked object to later assert it in the close(...) method.// It's important that we not store a reference to the referent as this would disallow it from// be collected via the WeakReference.trackedHash =System.identityHashCode(referent);allLeaks.add(this);// Create a new Record so we always have the creation stacktrace included.headUpdater.set(this,newRecord(Record.BOTTOM));this.allLeaks = allLeaks;}
在應(yīng)用中,對(duì)返回的ByteBuf對(duì)象進(jìn)行操作時(shí),都會(huì)間接調(diào)用 ResourceLeakDetector 的 reportLeak 方法:
privatevoid reportLeak(){if(!logger.isErrorEnabled()){clearRefQueue();return;}// Detect and report previous leaks.for(;;){@SuppressWarnings("unchecked")DefaultResourceLeakref=(DefaultResourceLeak) refQueue.poll();// 如果有弱引用被回收,則會(huì)進(jìn)入隊(duì)列if(ref==null){break;}if(!ref.dispose()){// 如果已經(jīng)釋放,則不是泄漏continue;}String records =ref.toString();if(reportedLeaks.putIfAbsent(records,Boolean.TRUE)==null){if(records.isEmpty()){reportUntracedLeak(resourceType);}else{reportTracedLeak(resourceType, records);}}}}
上述方法有以下要點(diǎn):
如果進(jìn)入refQueue,則說(shuō)明有弱引用被回收,如果dispose了,則說(shuō)明回收之前執(zhí)行release,釋放了資源,否則沒(méi)有釋放。
報(bào)告泄漏最近調(diào)用路徑
使用MAT進(jìn)行堆dump分析
Java引用類(lèi)型,還有一個(gè)用途點(diǎn),就是在使用eclipse mat工具時(shí),由于Java對(duì)象通過(guò)引用鏈接,所以當(dāng)找到一個(gè)大對(duì)象時(shí),可以過(guò)濾其他引用類(lèi)型,直接選擇強(qiáng)引用,從而一步一步最終拿到當(dāng)時(shí)調(diào)用鏈路棧:
這樣就能輕而易舉解決多數(shù)OOM問(wèn)題的根源。
總結(jié)
Java引用類(lèi)型很強(qiáng)大,可以來(lái)實(shí)現(xiàn)一些高校的應(yīng)用內(nèi)緩存,可以監(jiān)聽(tīng)gc動(dòng)作等。
根據(jù)部分類(lèi)文檔及結(jié)合代碼來(lái)確定 使用時(shí)要注意是否需要手動(dòng)釋放。
覺(jué)得對(duì)你有幫助?不如關(guān)注博主公眾號(hào): 六點(diǎn)A君
