小編給大家分享一下Netty怎么監(jiān)控內(nèi)存泄露,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
成都創(chuàng)新互聯(lián)長(zhǎng)期為1000多家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為延長(zhǎng)企業(yè)提供專業(yè)的網(wǎng)站設(shè)計(jì)、做網(wǎng)站,延長(zhǎng)網(wǎng)站改版等技術(shù)服務(wù)。擁有十載豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
一般而言,在Netty程序中都會(huì)采用池化的ByteBuf,也就是PooledByteBuf
以提高程序性能。但是PooledByteBuf
需要在使用完畢后手工釋放,否則就會(huì)因?yàn)?code>PooledByteBuf申請(qǐng)的內(nèi)存空間沒有歸還進(jìn)而造成內(nèi)存泄露,最終OOM。而一旦泄露發(fā)生,在復(fù)雜的應(yīng)用程序中找到未手工釋放的ByteBuf
并不是一個(gè)簡(jiǎn)單的活計(jì),在沒有工具輔助的情況只能白盒檢查所有源碼,效率無(wú)疑十分低下。
為了解決這個(gè)問題,Netty設(shè)計(jì)了專門的泄露檢測(cè)接口用于實(shí)現(xiàn)對(duì)需要手動(dòng)釋放的資源對(duì)象的監(jiān)控。
在分析Netty的泄露監(jiān)控功能之前,先來(lái)復(fù)習(xí)下其中會(huì)用到的JDK知識(shí):引用。
在java中存在4中引用類型,分別是強(qiáng)引用,軟引用,弱引用,虛引用。
強(qiáng)引用
強(qiáng)引用,是我們寫程序最經(jīng)常使用的方式。比如一個(gè)將一個(gè)值賦給一個(gè)變量,那這個(gè)對(duì)象值就被該變量強(qiáng)引用了。除非設(shè)置為null,否則java的內(nèi)存回收不會(huì)回收該對(duì)象。就算是內(nèi)存不足異常發(fā)生也不會(huì)。
軟引用
軟引用所引用的對(duì)象會(huì)在java內(nèi)存不足的時(shí)候,被gc回收。如果gc發(fā)生的時(shí)候,java的內(nèi)存還充足則不會(huì)回收這個(gè)對(duì)象 使用的方式如下
SoftReference ref = new SoftReference(new Date());
Date tmp = ref.get(); //如果對(duì)象沒有被回收,則這個(gè)get操作會(huì)返回初始化的值。如果被回收了之后,則返回null
弱引用
弱引用則比軟引用更差一些。只要是gc發(fā)生的時(shí)候,弱引用的對(duì)象都會(huì)被回收。使用方式上和軟引用類似,如下
WeakReference re = new WeakReference(new Date());
re.get();
虛引用
虛引用和前面的軟引用、弱引用不同,它并不影響對(duì)象的生命周期。在java中用java.lang.ref.PhantomReference
類表示。如果一個(gè)對(duì)象與虛引用關(guān)聯(lián),則跟沒有引用與之關(guān)聯(lián)一樣,在任何時(shí)候都可能被垃圾回收器回收。
除了強(qiáng)引用之外,其余的引用都有一個(gè)引用隊(duì)列可以與之配合。當(dāng)java清理調(diào)用不必要的引用后,會(huì)將這個(gè)引用本身(不是引用指向的值對(duì)象)添加到隊(duì)列之中。代碼如下
ReferenceQueue<Date> queue = new ReferenceQueue<>(); WeakReference<Date> re = new WeakReference<Date>(new Date(), queue); Reference<? extends Date> moved = queue.poll();
從上面的介紹可以看出引用隊(duì)列的一個(gè)適用場(chǎng)景:與弱引用或虛引用配合,監(jiān)控一個(gè)對(duì)象是否被GC回收。
針對(duì)需要手動(dòng)關(guān)閉的資源對(duì)象,Netty設(shè)計(jì)了一個(gè)接口io.netty.util.ResourceLeakTracker
來(lái)實(shí)現(xiàn)對(duì)資源對(duì)象的追蹤。該接口提供了一個(gè)release
方法。在資源對(duì)象關(guān)閉需要調(diào)用release
方法。如果從未調(diào)用release
方法則被認(rèn)為存在資源泄露。
該接口只有一個(gè)實(shí)現(xiàn),就是io.netty.util.ResourceLeakDetector.DefaultResourceLeak
,該實(shí)現(xiàn)繼承了WeakReference
。每一個(gè)DefaultResourceLeak
會(huì)與一個(gè)需要監(jiān)控的資源對(duì)象關(guān)聯(lián),同時(shí)關(guān)聯(lián)著一個(gè)引用隊(duì)列。
當(dāng)資源對(duì)象被GC回收后,與之關(guān)聯(lián)的DefaultResourceLeak
就會(huì)進(jìn)入引用隊(duì)列。通過(guò)檢查引用隊(duì)列中的DefaultResourceLeak
實(shí)例的狀態(tài)(release
方法的調(diào)用會(huì)導(dǎo)致狀態(tài)變更),就能確定在資源對(duì)象被GC前,是否執(zhí)行了手動(dòng)關(guān)閉的相關(guān)方法,從而判斷是否存在泄漏可能。
當(dāng)進(jìn)行ByteBuf的分配的時(shí)候,比如方法io.netty.buffer.PooledByteBufAllocator#newHeapBuffer
,查看代碼如下
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { PoolThreadCache cache = threadCache.get(); PoolArena<byte[]> heapArena = cache.heapArena; final ByteBuf buf; if (heapArena != null) { buf = heapArena.allocate(cache, initialCapacity, maxCapacity); } else { buf = PlatformDependent.hasUnsafe() ? new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) : new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity); } return toLeakAwareBuffer(buf); }
當(dāng)實(shí)際持有內(nèi)存區(qū)域的ByteBuf
生成,通過(guò)方法io.netty.buffer.AbstractByteBufAllocator#toLeakAwareBuffer(io.netty.buffer.ByteBuf)
加持監(jiān)控泄露的能力。該方法代碼如下
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) { ResourceLeakTracker<ByteBuf> leak; switch (ResourceLeakDetector.getLevel()) { case SIMPLE: leak = AbstractByteBuf.leakDetector.track(buf); if (leak != null) { buf = new SimpleLeakAwareByteBuf(buf, leak); } break; case ADVANCED: case PARANOID: leak = AbstractByteBuf.leakDetector.track(buf); if (leak != null) { buf = new AdvancedLeakAwareByteBuf(buf, leak); } break; default: break; } return buf; }
根據(jù)不同的監(jiān)控級(jí)別生成不同的監(jiān)控等級(jí)對(duì)象。Netty對(duì)監(jiān)控分為4個(gè)等級(jí):
關(guān)閉:這種模式下不進(jìn)行泄露監(jiān)控。
簡(jiǎn)單:這種模式下以1/128的概率抽取ByteBuf進(jìn)行泄露監(jiān)控。
增強(qiáng):在簡(jiǎn)單的基礎(chǔ)上,每一次對(duì)ByteBuf的調(diào)用都會(huì)嘗試記錄調(diào)用軌跡,消耗較大。
偏執(zhí):在增強(qiáng)的基礎(chǔ)上,對(duì)每一個(gè)ByteBuf都進(jìn)行泄露監(jiān)控,消耗最大。
一般而言,在項(xiàng)目的初期使用簡(jiǎn)單模式進(jìn)行監(jiān)控,如果沒有問題一段時(shí)間后就可以關(guān)閉。否則升級(jí)到增強(qiáng)或者偏執(zhí)模式嘗試確認(rèn)泄露位置。
泄露的檢查和追蹤主要依靠?jī)蓚€(gè)類io.netty.util.ResourceLeakDetector.DefaultResourceLeak
和io.netty.util.ResourceLeakDetector
.前者用于追蹤一個(gè)資源對(duì)象,并且記錄對(duì)應(yīng)的調(diào)用軌跡;后者則負(fù)責(zé)管理和生成DefaultResourceLeak
對(duì)象。
首先來(lái)看用于追蹤資源對(duì)象的監(jiān)控對(duì)象。該類繼承了WeakReference
,有幾個(gè)重要的屬性,如下
//存儲(chǔ)著最新的調(diào)用軌跡信息,record內(nèi)部通過(guò)next指針形成一個(gè)單向鏈表 private volatile Record head; //調(diào)用軌跡不會(huì)無(wú)限制的存儲(chǔ),有一個(gè)上限閥值。超過(guò)了閥值會(huì)拋棄掉一些調(diào)用軌跡信息。 private volatile int droppedRecords; //存儲(chǔ)著所有的追蹤對(duì)象,用于確認(rèn)追蹤對(duì)象是否處于可用。 private final Set<DefaultResourceLeak<?>> allLeaks; //記錄追蹤對(duì)象的hash值,用于后續(xù)操作中的對(duì)象對(duì)比。 private final int trackedHash;
這個(gè)類的作用有三個(gè):
調(diào)用record方法記錄調(diào)用軌跡
調(diào)用close方法結(jié)束追蹤
以及本身作為WeakReference
,在追蹤對(duì)象被GC回收后自身被入列到ReferenceQueue
中。
先來(lái)看下record
方法,代碼如下
@Override public void record() { record0(null); } @Override public void record(Object hint) { record0(hint); } private void record0(Object hint) { if (TARGET_RECORDS > 0) { Record oldHead; Record prevHead; Record newHead; boolean dropped; do { if ((prevHead = oldHead = headUpdater.get(this)) == null) { // already closed. return; } final int numElements = oldHead.pos + 1; if (numElements >= TARGET_RECORDS) { final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30); if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) { prevHead = oldHead.next; } } else { dropped = false; } newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead); } while (!headUpdater.compareAndSet(this, oldHead, newHead)); if (dropped) { droppedRecordsUpdater.incrementAndGet(this); } } }
方法record0
的思路總結(jié)下也很簡(jiǎn)單,概括如下:
使用CAS方式當(dāng)前的調(diào)用軌跡對(duì)象Record設(shè)置為head屬性的值。
Record
對(duì)象中的pos屬性記錄著當(dāng)前軌跡鏈的長(zhǎng)度,當(dāng)追蹤對(duì)象的軌跡隊(duì)鏈的長(zhǎng)度超過(guò)配置值時(shí),有一定的幾率(1-1/2<sup>min(n-target_record,30)</sup>)將最新的軌跡對(duì)象從鏈條中刪除。
CAS成功后,如果有拋棄頭部的軌跡對(duì)象,則拋棄計(jì)數(shù)+1。
步驟2中在鏈條過(guò)長(zhǎng)時(shí)選擇刪除最新的軌跡對(duì)象是基于以下兩點(diǎn)出發(fā):
一般泄漏都發(fā)生在最后一次使用后忘記調(diào)用釋放方法造成,因此替換最新的歸集對(duì)象,并不會(huì)造成判斷信息的丟失
一般而言,關(guān)注泄漏對(duì)象,也需要了解對(duì)象實(shí)例的申請(qǐng)位置,因此刪除節(jié)點(diǎn)時(shí)不能從頭開始刪除。
在來(lái)看看close
方法。代碼如下
public boolean close(T trackedObject) { assert trackedHash == System.identityHashCode(trackedObject); try { return close(); } finally { reachabilityFence0(trackedObject); } } public boolean close() { if (allLeaks.remove(this)) { // Call clear so the reference is not even enqueued. clear(); headUpdater.set(this, null); return true; } return false; } private static void reachabilityFence0(Object ref) { if (ref != null) { synchronized (ref) { } } }
close
方法本身沒有什么,就是將資源進(jìn)行了清除。需要解釋的是方法reachabilityFence0
。不過(guò)該方法需要在下文的報(bào)告泄露中才會(huì)具備作用,這邊先暫留。
該類用于按照規(guī)則進(jìn)行追蹤對(duì)象的生成,外部主要是調(diào)用其方法track
,代碼如下
public final ResourceLeakTracker<T> track(T obj) { return track0(obj); } private DefaultResourceLeak track0(T obj) { Level level = ResourceLeakDetector.level; if (level == Level.DISABLED) { return null; } if (level.ordinal() < Level.PARANOID.ordinal()) { if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) { reportLeak(); return new DefaultResourceLeak(obj, refQueue, allLeaks); } return null; } reportLeak(); return new DefaultResourceLeak(obj, refQueue, allLeaks); }
從生成策略來(lái)看,只要是小于PARANOID
級(jí)別都是抽樣生成。生成的追蹤對(duì)象上一個(gè)章節(jié)已經(jīng)分析過(guò)了,這邊主要來(lái)看reportLeak
方法,如下
private void reportLeak() { if (!logger.isErrorEnabled()) { clearRefQueue(); return; } // Detect and report previous leaks. for (;;) { @SuppressWarnings("unchecked") DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll(); if (ref == null) { break; } //返回true意味著資源沒有調(diào)用close或者dispose方法結(jié)束追蹤就被GC了,意味著該資源存在泄漏。 if (!ref.dispose()) { continue; } String records = ref.toString(); if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) { if (records.isEmpty()) { reportUntracedLeak(resourceType); } else { reportTracedLeak(resourceType, records); } } } } boolean io.netty.util.ResourceLeakDetector.DefaultResourceLeak#dispose() { clear(); return allLeaks.remove(this); }
可以看到,每次生成資源追蹤對(duì)象時(shí),都會(huì)遍歷引用隊(duì)列,如果發(fā)現(xiàn)泄漏對(duì)象,則進(jìn)行日志輸出。
這里面有個(gè)細(xì)節(jié)的設(shè)計(jì)點(diǎn)在于DefaultResourceLeak
進(jìn)入引用隊(duì)列并不意味著一定內(nèi)存泄露。判斷追蹤對(duì)象是否泄漏的規(guī)則是對(duì)象在被GC之前是否調(diào)用了DefaultResourceLeak
的close
方法。舉個(gè)例子,PooledByteBuf
只要將自身持有的內(nèi)存釋放回池化區(qū)就算是正確的釋放,其后其實(shí)例對(duì)象可以被GC回收掉。
因此方法reportLeak
在遍歷引用隊(duì)列時(shí),需要通過(guò)調(diào)用dispose
方法來(lái)確認(rèn)追蹤對(duì)象的dispose
是否調(diào)用或者close
方法是否被調(diào)用過(guò)。如果dispose
方法返回true,則意味著被追蹤對(duì)象未調(diào)用關(guān)閉方法就被GC,那就意味著造成了泄露。
上個(gè)章節(jié)曾提到的一個(gè)方法reachabilityFence0
。
在JVM的規(guī)定中,如果一個(gè)實(shí)例對(duì)象不再被需要,則可以判定為可回收。即使該實(shí)例對(duì)象的一個(gè)具體方法正在執(zhí)行過(guò)程中,也是可以的。更確切一些的說(shuō),如果一個(gè)實(shí)例對(duì)象的方法體中,不再需要讀取或者寫入實(shí)例對(duì)象的屬性,則此時(shí)JVM可以回收該對(duì)象,即使方法還沒有完成。
然而這樣會(huì)導(dǎo)致一個(gè)問題,在close方法中,如果close方法還沒有執(zhí)行完畢,trackedObject
對(duì)象實(shí)例就被GC回收了,就會(huì)導(dǎo)致DefaultResourceLeak
對(duì)象被加入到引用隊(duì)列中,從而可能在reportLeak
方法調(diào)用中觸發(fā)方法dispose
,假設(shè)此時(shí)close
方法才剛開始執(zhí)行,則dispose
方法可能返回true。程序就會(huì)判定這個(gè)對(duì)象出現(xiàn)了泄露,然而實(shí)際上卻沒有。
要解決這個(gè)問題,只需要讓close
方法執(zhí)行完畢前,讓對(duì)象不要回收即可。reachabilityFence0
方法就完成了這個(gè)作用。
以上是“Netty怎么監(jiān)控內(nèi)存泄露”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!
名稱欄目:Netty怎么監(jiān)控內(nèi)存泄露
網(wǎng)頁(yè)地址:http://muchs.cn/article36/ihgssg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站建設(shè)、網(wǎng)站營(yíng)銷、虛擬主機(jī)、Google、微信小程序、網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)