您好,登錄后才能下訂單哦!
小編給大家分享一下Netty怎么監控內存泄露,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
一般而言,在Netty程序中都會采用池化的ByteBuf,也就是PooledByteBuf
以提高程序性能。但是PooledByteBuf
需要在使用完畢后手工釋放,否則就會因為PooledByteBuf
申請的內存空間沒有歸還進而造成內存泄露,最終OOM。而一旦泄露發生,在復雜的應用程序中找到未手工釋放的ByteBuf
并不是一個簡單的活計,在沒有工具輔助的情況只能白盒檢查所有源碼,效率無疑十分低下。
為了解決這個問題,Netty設計了專門的泄露檢測接口用于實現對需要手動釋放的資源對象的監控。
在分析Netty的泄露監控功能之前,先來復習下其中會用到的JDK知識:引用。
在java中存在4中引用類型,分別是強引用,軟引用,弱引用,虛引用。
強引用
強引用,是我們寫程序最經常使用的方式。比如一個將一個值賦給一個變量,那這個對象值就被該變量強引用了。除非設置為null,否則java的內存回收不會回收該對象。就算是內存不足異常發生也不會。
軟引用
軟引用所引用的對象會在java內存不足的時候,被gc回收。如果gc發生的時候,java的內存還充足則不會回收這個對象 使用的方式如下
SoftReference ref = new SoftReference(new Date());
Date tmp = ref.get(); //如果對象沒有被回收,則這個get操作會返回初始化的值。如果被回收了之后,則返回null
弱引用
弱引用則比軟引用更差一些。只要是gc發生的時候,弱引用的對象都會被回收。使用方式上和軟引用類似,如下
WeakReference re = new WeakReference(new Date());
re.get();
虛引用
虛引用和前面的軟引用、弱引用不同,它并不影響對象的生命周期。在java中用java.lang.ref.PhantomReference
類表示。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。
除了強引用之外,其余的引用都有一個引用隊列可以與之配合。當java清理調用不必要的引用后,會將這個引用本身(不是引用指向的值對象)添加到隊列之中。代碼如下
ReferenceQueue<Date> queue = new ReferenceQueue<>(); WeakReference<Date> re = new WeakReference<Date>(new Date(), queue); Reference<? extends Date> moved = queue.poll();
從上面的介紹可以看出引用隊列的一個適用場景:與弱引用或虛引用配合,監控一個對象是否被GC回收。
針對需要手動關閉的資源對象,Netty設計了一個接口io.netty.util.ResourceLeakTracker
來實現對資源對象的追蹤。該接口提供了一個release
方法。在資源對象關閉需要調用release
方法。如果從未調用release
方法則被認為存在資源泄露。
該接口只有一個實現,就是io.netty.util.ResourceLeakDetector.DefaultResourceLeak
,該實現繼承了WeakReference
。每一個DefaultResourceLeak
會與一個需要監控的資源對象關聯,同時關聯著一個引用隊列。
當資源對象被GC回收后,與之關聯的DefaultResourceLeak
就會進入引用隊列。通過檢查引用隊列中的DefaultResourceLeak
實例的狀態(release
方法的調用會導致狀態變更),就能確定在資源對象被GC前,是否執行了手動關閉的相關方法,從而判斷是否存在泄漏可能。
當進行ByteBuf的分配的時候,比如方法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); }
當實際持有內存區域的ByteBuf
生成,通過方法io.netty.buffer.AbstractByteBufAllocator#toLeakAwareBuffer(io.netty.buffer.ByteBuf)
加持監控泄露的能力。該方法代碼如下
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; }
根據不同的監控級別生成不同的監控等級對象。Netty對監控分為4個等級:
關閉:這種模式下不進行泄露監控。
簡單:這種模式下以1/128的概率抽取ByteBuf進行泄露監控。
增強:在簡單的基礎上,每一次對ByteBuf的調用都會嘗試記錄調用軌跡,消耗較大。
偏執:在增強的基礎上,對每一個ByteBuf都進行泄露監控,消耗最大。
一般而言,在項目的初期使用簡單模式進行監控,如果沒有問題一段時間后就可以關閉。否則升級到增強或者偏執模式嘗試確認泄露位置。
泄露的檢查和追蹤主要依靠兩個類io.netty.util.ResourceLeakDetector.DefaultResourceLeak
和io.netty.util.ResourceLeakDetector
.前者用于追蹤一個資源對象,并且記錄對應的調用軌跡;后者則負責管理和生成DefaultResourceLeak
對象。
首先來看用于追蹤資源對象的監控對象。該類繼承了WeakReference
,有幾個重要的屬性,如下
//存儲著最新的調用軌跡信息,record內部通過next指針形成一個單向鏈表 private volatile Record head; //調用軌跡不會無限制的存儲,有一個上限閥值。超過了閥值會拋棄掉一些調用軌跡信息。 private volatile int droppedRecords; //存儲著所有的追蹤對象,用于確認追蹤對象是否處于可用。 private final Set<DefaultResourceLeak<?>> allLeaks; //記錄追蹤對象的hash值,用于后續操作中的對象對比。 private final int trackedHash;
這個類的作用有三個:
調用record方法記錄調用軌跡
調用close方法結束追蹤
以及本身作為WeakReference
,在追蹤對象被GC回收后自身被入列到ReferenceQueue
中。
先來看下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
的思路總結下也很簡單,概括如下:
使用CAS方式當前的調用軌跡對象Record設置為head屬性的值。
Record
對象中的pos屬性記錄著當前軌跡鏈的長度,當追蹤對象的軌跡隊鏈的長度超過配置值時,有一定的幾率(1-1/2<sup>min(n-target_record,30)</sup>)將最新的軌跡對象從鏈條中刪除。
CAS成功后,如果有拋棄頭部的軌跡對象,則拋棄計數+1。
步驟2中在鏈條過長時選擇刪除最新的軌跡對象是基于以下兩點出發:
一般泄漏都發生在最后一次使用后忘記調用釋放方法造成,因此替換最新的歸集對象,并不會造成判斷信息的丟失
一般而言,關注泄漏對象,也需要了解對象實例的申請位置,因此刪除節點時不能從頭開始刪除。
在來看看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
方法本身沒有什么,就是將資源進行了清除。需要解釋的是方法reachabilityFence0
。不過該方法需要在下文的報告泄露中才會具備作用,這邊先暫留。
該類用于按照規則進行追蹤對象的生成,外部主要是調用其方法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); }
從生成策略來看,只要是小于PARANOID
級別都是抽樣生成。生成的追蹤對象上一個章節已經分析過了,這邊主要來看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意味著資源沒有調用close或者dispose方法結束追蹤就被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); }
可以看到,每次生成資源追蹤對象時,都會遍歷引用隊列,如果發現泄漏對象,則進行日志輸出。
這里面有個細節的設計點在于DefaultResourceLeak
進入引用隊列并不意味著一定內存泄露。判斷追蹤對象是否泄漏的規則是對象在被GC之前是否調用了DefaultResourceLeak
的close
方法。舉個例子,PooledByteBuf
只要將自身持有的內存釋放回池化區就算是正確的釋放,其后其實例對象可以被GC回收掉。
因此方法reportLeak
在遍歷引用隊列時,需要通過調用dispose
方法來確認追蹤對象的dispose
是否調用或者close
方法是否被調用過。如果dispose
方法返回true,則意味著被追蹤對象未調用關閉方法就被GC,那就意味著造成了泄露。
上個章節曾提到的一個方法reachabilityFence0
。
在JVM的規定中,如果一個實例對象不再被需要,則可以判定為可回收。即使該實例對象的一個具體方法正在執行過程中,也是可以的。更確切一些的說,如果一個實例對象的方法體中,不再需要讀取或者寫入實例對象的屬性,則此時JVM可以回收該對象,即使方法還沒有完成。
然而這樣會導致一個問題,在close方法中,如果close方法還沒有執行完畢,trackedObject
對象實例就被GC回收了,就會導致DefaultResourceLeak
對象被加入到引用隊列中,從而可能在reportLeak
方法調用中觸發方法dispose
,假設此時close
方法才剛開始執行,則dispose
方法可能返回true。程序就會判定這個對象出現了泄露,然而實際上卻沒有。
要解決這個問題,只需要讓close
方法執行完畢前,讓對象不要回收即可。reachabilityFence0
方法就完成了這個作用。
以上是“Netty怎么監控內存泄露”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。