[TOC]java
通常而言,在Netty程序中都會採用池化的ByteBuf,也就是PooledByteBuf
以提升程序性能。可是PooledByteBuf
須要在使用完畢後手工釋放,不然就會由於PooledByteBuf
申請的內存空間沒有歸還進而形成內存泄露,最終OOM。而一旦泄露發生,在複雜的應用程序中找到未手工釋放的ByteBuf
並非一個簡單的活計,在沒有工具輔助的狀況只能白盒檢查全部源碼,效率無疑十分低下。dom
爲了解決這個問題,Netty設計了專門的泄露檢測接口用於實現對須要手動釋放的資源對象的監控。ide
在分析Netty的泄露監控功能以前,先來複習下其中會用到的JDK知識:引用。工具
在java中存在4中引用類型,分別是強引用,軟引用,弱引用,虛引用。性能
強引用this
強引用,是咱們寫程序最常用的方式。好比一個將一個值賦給一個變量,那這個對象值就被該變量強引用了。除非設置爲null,不然java的內存回收不會回收該對象。就算是內存不足異常發生也不會。spa
軟引用.net
軟引用所引用的對象會在java內存不足的時候,被gc回收。若是gc發生的時候,java的內存還充足則不會回收這個對象 使用的方式以下設計
弱引用指針
弱引用則比軟引用更差一些。只要是gc發生的時候,弱引用的對象都會被回收。使用方式上和軟引用相似,以下
虛引用
虛引用和前面的軟引用、弱引用不一樣,它並不影響對象的生命週期。在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個等級:
通常而言,在項目的初期使用簡單模式進行監控,若是沒有問題一段時間後就能夠關閉。不然升級到加強或者偏執模式嘗試確認泄露位置。
泄露的檢查和追蹤主要依靠兩個類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;
複製代碼
這個類的做用有三個:
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
的思路總結下也很簡單,歸納以下:
Record
對象中的pos屬性記錄着當前軌跡鏈的長度,當追蹤對象的軌跡隊鏈的長度超過配置值時,有必定的概率(1-1/2min(n-target_record,30))將最新的軌跡對象從鏈條中刪除。步驟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
方法就完成了這個做用。
文章原創首發於公衆號:林斌說Java,轉載請註明來源,謝謝。
歡迎掃碼關注