本文基于 Netty 4.1.112.Final 版本進(jìn)行討論
本文是 Netty 內(nèi)存管理系列的最后一篇文章,在第一篇文章 《聊一聊 Netty 數(shù)據(jù)搬運(yùn)工 ByteBuf 體系的設(shè)計(jì)與實(shí)現(xiàn)》 中,筆者以 UnpooledByteBuf 為例,從整個(gè)內(nèi)存管理的外圍對(duì) ByteBuf 的整個(gè)設(shè)計(jì)體系進(jìn)行了詳細(xì)的拆解剖析,隨后在第二篇文章 《談一談 Netty 的內(nèi)存管理 —— 且看 Netty 如何實(shí)現(xiàn) Java 版的 Jemalloc》 中,筆者又帶大家深入到 Netty 內(nèi)存池的內(nèi)部,對(duì)整個(gè)池化內(nèi)存的管理進(jìn)行了詳細(xì)拆解。
不知大家有沒(méi)有注意到,無(wú)論是非池化內(nèi)存 —— UnpooledByteBuf 的分配還是池化內(nèi)存 —— PooledByteBuf 的分配,最后都會(huì)被 Netty 包裝成一個(gè) LeakAwareBuffer 返回。
public final class UnpooledByteBufAllocator {
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
final ByteBuf buf;
if (PlatformDependent.hasUnsafe()) {
buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
// 是否啟動(dòng)內(nèi)存泄露探測(cè),如果啟動(dòng)則額外用 LeakAwareByteBuf 進(jìn)行包裝返回
return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
}
}
public class PooledByteBufAllocator {
// 線程本地緩存
private final PoolThreadLocalCache threadCache;
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
// 獲取線程本地緩存,線程第一次申請(qǐng)內(nèi)存的時(shí)候會(huì)在這里與 PoolArena 進(jìn)行綁定
PoolThreadCache cache = threadCache.get();
// 獲取與當(dāng)前線程綁定的 PoolArena
PoolArena<ByteBuffer> directArena = cache.directArena;
final ByteBuf buf;
if (directArena != null) {
// 從固定的 PoolArena 中申請(qǐng)內(nèi)存
buf = directArena.allocate(cache, initialCapacity, maxCapacity);
} else {
// 申請(qǐng)非池化內(nèi)存
buf = PlatformDependent.hasUnsafe() ?
UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
// 如果內(nèi)存泄露探測(cè)開(kāi)啟,則用 LeakAwareByteBuf 包裝 PooledByteBuf 返回
return toLeakAwareBuffer(buf);
}
}
筆者之前曾提到過(guò),相比于 JDK DirectByteBuffer 需要依賴 GC 機(jī)制來(lái)釋放其背后引用的 Native Memory , Netty 更傾向于手動(dòng)及時(shí)釋放 DirectByteBuf 。因?yàn)?JDK DirectByteBuffer 的釋放需要等到 GC 發(fā)生,由于 DirectByteBuffer 的對(duì)象實(shí)例所占的 JVM 堆內(nèi)存太小了,所以一時(shí)很難觸發(fā) GC , 這就導(dǎo)致被引用的 Native Memory 的釋放有了一定的延遲,嚴(yán)重的情況會(huì)越積越多,導(dǎo)致 OOM 。而且也會(huì)導(dǎo)致進(jìn)程中對(duì) DirectByteBuffer 的申請(qǐng)操作有非常大的延遲。
而 Netty 為了避免這些情況的出現(xiàn),選擇在每次使用完之后手動(dòng)釋放 Native Memory ,但是不依賴 JVM 的話,總會(huì)有內(nèi)存泄露的情況,比如在使用完了 ByteBuf 卻忘記調(diào)用 release() 方法釋放。
手動(dòng)釋放雖然及時(shí)可控,但是卻很容易出現(xiàn)內(nèi)存泄露。Netty 為了應(yīng)對(duì)內(nèi)存泄露的發(fā)生,從而引入了 LeakAwareBuffer,從命名上就可以看出,LeakAwareBuffer 主要是為了識(shí)別出被其包裝的 ByteBuf 是否有內(nèi)存泄露情況的發(fā)生。
現(xiàn)在大家是不是對(duì)這個(gè) LeakAwareBuffer 非常的好奇,它究竟擁有怎樣的魔力,居然能夠自動(dòng)探測(cè)內(nèi)存泄露,但現(xiàn)在我們先把 LeakAwareBuffer 丟在一邊,先不用管它,因?yàn)樗皇?ByteBuf 一個(gè)簡(jiǎn)單的套殼,背后真正核心的是與內(nèi)存泄露相關(guān)的一些探測(cè)模型設(shè)計(jì),所以筆者決定先從最核心的設(shè)計(jì)原理開(kāi)始談起~~~

1. 內(nèi)存泄露探測(cè)的設(shè)計(jì)原理
首先我們來(lái)看第一個(gè)核心的問(wèn)題,我們究竟該選擇一個(gè)什么樣的時(shí)機(jī)來(lái)對(duì)內(nèi)存泄露進(jìn)行探測(cè) ?
正在使用的內(nèi)存肯定不能算是泄露,別管我已經(jīng)消耗了多么大的內(nèi)存,但這些內(nèi)存確實(shí)是正在使用的,你不能說(shuō)我是內(nèi)存泄露對(duì)吧。當(dāng)我不需要這些內(nèi)存了,但仍然繼續(xù)持有著不釋放,這種情況,我們才能定義為內(nèi)存泄露。
所以當(dāng)內(nèi)存不再被使用的時(shí)候,才是我們進(jìn)行內(nèi)存泄露探測(cè)的時(shí)機(jī),而正在使用的內(nèi)存,壓根就沒(méi)有內(nèi)存泄露,自然也不需要進(jìn)行探測(cè),那么接下來(lái)的問(wèn)題就是,我們?nèi)绾闻袛嗄骋粔K內(nèi)存是正在被使用的 ? 還是已經(jīng)不在被使用了 ?
那肯定得靠 GC ??!對(duì)吧。當(dāng)一個(gè) DirectByteBuf 已經(jīng)沒(méi)有任何強(qiáng)引用或者軟引用的時(shí)候,那就說(shuō)明它已經(jīng)不在被使用了,GC 就會(huì)回收它。當(dāng)它還存在強(qiáng)引用或者軟引用的時(shí)候,說(shuō)明它還在被使用,那么 GC 就不會(huì)回收它。
但是內(nèi)存泄露探測(cè)的功能是在 JVM 之外實(shí)現(xiàn)的,JVM 不會(huì)意識(shí)到我們到底想要干嘛,它只管無(wú)腦回收 DirectByteBuf,對(duì)于 DirectByteBuf 背后引用的 Native Memory 是否發(fā)生泄露,JVM 壓根就不會(huì) Care 。
看上去靠 GC 是靠不住了,但如果我們能夠在 DirectByteBuf 被 GC 的時(shí)候得到一個(gè) JVM 的通知,然后在這個(gè)通知中,觸發(fā)內(nèi)存泄露的探測(cè),是不是就可以了 ?那我們?nèi)绾蔚玫竭@個(gè)通知呢 ?
還記不記得筆者在 《以 ZGC 為例,談一談 JVM 是如何實(shí)現(xiàn) Reference 語(yǔ)義的》 一文中介紹的 WeakReference 和 PhantomReference 以及 FinalReference ? 它們都可以拿到這個(gè)通知。
比如 JDK 中的 DirectByteBuffer ,其背后引用的 Native Memory 的回收需要依靠 Cleaner 機(jī)制,而 Cleaner 就是一個(gè) PhantomReference 對(duì)象。
public class Cleaner extends PhantomReference<Object>

Cleaner 虛引用了 DirectByteBuffer,這樣一來(lái)當(dāng)這個(gè) DirectByteBuffer 沒(méi)有任何強(qiáng)引用或者軟引用的時(shí)候,也就是不會(huì)再被使用了,后面就會(huì)被 GC 回收掉,與此同時(shí) JVM 會(huì)將它的虛引用 Cleaner 放入 JVM 內(nèi)部一個(gè)叫做 _reference_pending_list 的鏈表中。
隨后 JVM 會(huì)喚醒 JDK 中的 1 號(hào)線程 —— ReferenceHandler。
Thread handler = new ReferenceHandler(tg, "Reference Handler");
// 設(shè)置 ReferenceHandler 線程的優(yōu)先級(jí)為最高優(yōu)先級(jí)
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
ReferenceHandler 線程會(huì)從 JVM 的 _reference_pending_list 中挨個(gè)將所有的 Cleaner 摘下,調(diào)用它的 clean() 方法,最終在 Deallocator 中釋放 Native Memory 。
private static class Deallocator implements Runnable {
public void run() {
// 底層調(diào)用 free 來(lái)釋放 native memory
UNSAFE.freeMemory(address);
}
}

再比如 Netty 內(nèi)存池中的線程本地緩存 PoolThreadCache,其背后緩存的池化 Native Memory 的回收依賴的是 Finalizer 機(jī)制。
private static final class FreeOnFinalize {
// 待釋放的 PoolThreadCache
private volatile PoolThreadCache cache;
private FreeOnFinalize(PoolThreadCache cache) {
this.cache = cache;
}
@Override
protected void finalize() throws Throwable {
try {
super.finalize();
} finally {
PoolThreadCache cache = this.cache;
this.cache = null;
// 當(dāng) FreeOnFinalize 實(shí)例要被回收的時(shí)候,觸發(fā) PoolThreadCache 的釋放
if (cache != null) {
cache.free(true);
}
}
}
}
FreeOnFinalize 的作用主要就是為了回收 PoolThreadCache , 內(nèi)部重寫(xiě)了 finalize() 方法,JVM 會(huì)為其創(chuàng)建一個(gè) Finalizer 對(duì)象(FinalReference 類型),F(xiàn)inalizer 引用了 FreeOnFinalize ,但這種引用關(guān)系是一種 FinalReference 類型。
final class Finalizer extends FinalReference<Object> {
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private Finalizer(Object finalizee) {
// 這里的 finalizee 就是 FreeOnFinalize 對(duì)象,被 FinalReference 引用
super(finalizee, queue);
......
}
}

Finalizer 中有一個(gè)全局的 ReferenceQueue,這個(gè) ReferenceQueue 非常的重要,因?yàn)?JVM 中的 _reference_pending_list 是屬于 JVM 內(nèi)部的,除了 ReferenceHandler 線程,其它普通的 Java 線程是訪問(wèn)不了的,所以我們要想在 JVM 的外部處理這些 Reference(其引用的對(duì)象已經(jīng)被回收),就需要用到一個(gè)外部隊(duì)列,這個(gè)外部隊(duì)列就是 Finalizer 中的 ReferenceQueue。
Reference(T referent, ReferenceQueue<? super T> queue) {
// FreeOnFinalize 對(duì)象
this.referent = referent;
// Finalizer 中的 ReferenceQueue 實(shí)例(全局)
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
當(dāng)線程終結(jié)的時(shí)候,那么 PoolThreadCache 與 FreeOnFinalize 對(duì)象將會(huì)被 GC 回收,但由于 FreeOnFinalize 被一個(gè) FinalReference(Finalizer) 引用,所以 JVM 會(huì)將 FreeOnFinalize 對(duì)象再次復(fù)活,由于 FreeOnFinalize 對(duì)象也引用了 PoolThreadCache,所以 PoolThreadCache 也會(huì)被復(fù)活。
隨后 JVM 會(huì)將這個(gè) Finalizer(FinalReference 對(duì)象)放入到內(nèi)部 _reference_pending_list 中,然后 ReferenceHandler 線程會(huì)從 _reference_pending_list 中將 Finalizer 對(duì)象挨個(gè)摘下,并將其放入到 ReferenceQueue 中。
最后 JDK 中的 2 號(hào)線程 —— FinalizerThread 被喚醒,從 ReferenceQueue 中將收集到的 Finalizer 對(duì)象挨個(gè)摘下,并執(zhí)行它的 runFinalizer 方法,最終在 FreeOnFinalize 對(duì)象的 finalize() 方法中將 PoolThreadCache 釋放。
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();

以上就是針對(duì) Native Memory 回收的一些例子實(shí)現(xiàn),同樣的道理,關(guān)于 Native Memory 的泄露探測(cè)也是一樣,它們的共同觸發(fā)時(shí)機(jī)都是需要等到 DirectByteBuf 不在被使用的時(shí)候,也就是被 GC 的時(shí)候。
Netty 這里使用了 WeakReference 來(lái)獲取 DirectByteBuf 被 GC 的通知。
final class DefaultResourceLeak<T> extends WeakReference<Object>

前面筆者提過(guò),_reference_pending_list 是一個(gè) JVM 內(nèi)部的隊(duì)列,如果我們想要在 JVM 外部處理 DefaultResourceLeak ,就必須在創(chuàng)建 DefaultResourceLeak 的時(shí)候傳入一個(gè)全局的 ReferenceQueue,Netty 用于內(nèi)存泄露探測(cè)的 ReferenceQueue 定義在 ResourceLeakDetector 中。
public class ResourceLeakDetector<T> {
private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
}
有了這個(gè) ReferenceQueue 之后,當(dāng) DirectByteBuf 在系統(tǒng)中沒(méi)有任何強(qiáng)引用或者軟引用的時(shí)候,那么就只剩下一個(gè)弱引用 DefaultResourceLeak 在引用它了,這時(shí) DirectByteBuf 就會(huì)被 GC 回收,后面的 WeakReference 處理流程和前面的 PhantomReference , FinalReference 都是一樣的。
JVM 會(huì)將 DefaultResourceLeak 放入到內(nèi)部的 _reference_pending_list 中,隨后 ReferenceHandler 線程會(huì)從 _reference_pending_list 中將 DefaultResourceLeak 摘下,并將它放入到與其關(guān)聯(lián)的 ReferenceQueue 中,這里的 ReferenceQueue 就是 ResourceLeakDetector 中定義的全局 refQueue,會(huì)在創(chuàng)建 DefaultResourceLeak 對(duì)象的時(shí)候傳入。
當(dāng)這個(gè) DefaultResourceLeak 對(duì)象被 ReferenceHandler 線程放入到 ReferenceQueue 之后,后面的處理流程就和前面的不一樣了。
Cleaner 是由 ReferenceHandler 線程直接進(jìn)行處理,F(xiàn)inalizer 是由 FinalizerThread 線程進(jìn)行處理,那這里的 DefaultResourceLeak 又該由哪個(gè)線程來(lái)處理呢 ?這是我們面臨的第二個(gè)核心問(wèn)題。
Cleaner 與 Finalizer 都是 JDK 內(nèi)部實(shí)現(xiàn)的一個(gè)機(jī)制,所以 JDK 都會(huì)配有專門(mén)的守護(hù)線程來(lái)處理它們,而 DefaultResourceLeak 是 Netty 在 JDK 外部實(shí)現(xiàn)的內(nèi)存泄露探測(cè)機(jī)制,Netty 不可能專門(mén)起一個(gè)守護(hù)線程來(lái)處理內(nèi)存泄露的探測(cè),也沒(méi)這個(gè)必要。
事實(shí)上,Netty 中的任何一個(gè)線程都可以處理 DefaultResourceLeak,因?yàn)閮?nèi)存分配是一個(gè)非常頻繁的操作,在分配內(nèi)存的時(shí)候順帶探測(cè)一下是否有內(nèi)存泄露的情況發(fā)生就可以了,沒(méi)有必要專門(mén)配備一個(gè)線程來(lái)探測(cè)內(nèi)存泄露。這樣資源消耗不僅少,內(nèi)存泄露探測(cè)的還更快更及時(shí)一些。
當(dāng)某一個(gè)線程在調(diào)用 ByteBufAllocator 申請(qǐng)內(nèi)存的時(shí)候,Netty 就會(huì)觸發(fā)對(duì) ReferenceQueue 的檢測(cè),如果隊(duì)列中包含 DefaultResourceLeak 就將它拿下來(lái)檢查一下是否有內(nèi)存泄露發(fā)生。那么我們依據(jù)什么來(lái)判斷一個(gè) DirectByteBuf 是否發(fā)生內(nèi)存泄露呢 ?這是我們面臨的第三個(gè)核心問(wèn)題。
Netty 為每個(gè) ByteBuf 都維護(hù)了一個(gè)引用計(jì)數(shù) —— refCnt 。
public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
// 引用計(jì)數(shù)
private volatile int refCnt;
}
我們可以通過(guò) refCnt() 方法來(lái)獲取 ByteBuf 當(dāng)前的引用計(jì)數(shù) refCnt。當(dāng) ByteBuf 在其他上下文中被引用的時(shí)候,我們需要通過(guò) retain()方法將 ByteBuf 的引用計(jì)數(shù)加 1。每當(dāng)我們使用完 ByteBuf 的時(shí)候就需要手動(dòng)調(diào)用 release() 方法將 ByteBuf 的引用計(jì)數(shù)減 1 。當(dāng)引用計(jì)數(shù) refCnt 變成 0 的時(shí)候,Netty 就會(huì)通過(guò) deallocate 方法來(lái)釋放 ByteBuf 所引用的 Native Memory。
public interface ReferenceCounted {
int refCnt();
ReferenceCounted retain();
boolean release();
}
于是我們很容易想到能不能在這個(gè)引用計(jì)數(shù) refCnt 身上做做文章,當(dāng)一個(gè) DirectByteBuf 被 GC 的時(shí)候,如果它的引用計(jì)數(shù)為 0 ,表示它引用的 Native Memory 已經(jīng)及時(shí)地被釋放掉了,不存在內(nèi)存泄露。如果它的引用計(jì)數(shù)不為 0 ,那就說(shuō)明它背后引用的 Native Memory 沒(méi)有被釋放,內(nèi)存泄露就發(fā)生了。
想法很好,但是非??上?,我們現(xiàn)在已經(jīng)拿不到 DirectByteBuf 了,它的引用計(jì)數(shù)更是無(wú)從獲取,因?yàn)樗呀?jīng)被 GC 了,而現(xiàn)在我們只能從 ReferenceQueue 中拿到與 DirectByteBuf 弱引用關(guān)聯(lián)的 DefaultResourceLeak 。那該怎么辦呢 ?
我們判斷一個(gè) DirectByteBuf 是否存在內(nèi)存泄露最根本的依據(jù)還是要看它的引用計(jì)數(shù)是否為 0 ,但現(xiàn)在 DirectByteBuf 已經(jīng)被 GC 了,它的引用計(jì)數(shù)也獲取不到了,但是我們還可以在另一個(gè)維度實(shí)現(xiàn) “引用計(jì)數(shù)是否為 0” 的這層語(yǔ)義 —— 曲線救國(guó)。
如何實(shí)現(xiàn)呢 ? 我們還是重新到 Cleaner 和 Finalizer 機(jī)制中去找找靈感,在 Cleaner 的內(nèi)部都會(huì)有一個(gè)全局的雙向鏈表 —— first 。
public class Cleaner extends PhantomReference<Object>
{
private static Cleaner first = null;
private Cleaner next = null, prev = null;
}

每當(dāng)一個(gè) Cleaner 對(duì)象被創(chuàng)建出來(lái)之后,JDK 就會(huì)將新的 Cleaner 對(duì)象采用頭插法插入到該雙向鏈表中。
這么做的目的就是為了讓系統(tǒng)中的這些 Cleaner 對(duì)象始終與 GcRoot 關(guān)聯(lián),始終保持一條強(qiáng)引用鏈的存在。
這樣一來(lái)就可以保證被 Cleaner 對(duì)象虛引用的這個(gè) DirectByteBuffer 對(duì)象,無(wú)論在它被 GC 回收之前還是回收之后,與它關(guān)聯(lián)的這個(gè) Cleaner 對(duì)象始終保持活躍不會(huì)被 GC 回收掉,因?yàn)槲覀冏罱K要依靠這個(gè) Cleaner 對(duì)象來(lái)釋放 native memory 。
同理,為了確保這些 Finalizer 在執(zhí)行 finalizee 對(duì)象的 finalize() 方法之前不會(huì)被 GC 回收掉。Finalizer 的內(nèi)部也有一個(gè)雙向鏈表 —— unfinalized,用來(lái)強(qiáng)引用 JVM 堆中所有的 Finalizer 對(duì)象。
final class Finalizer extends FinalReference<Object> {
// 雙向鏈表,保存 JVM 堆中所有的 Finalizer 對(duì)象,防止 Finalizer 被 GC 掉
private static Finalizer unfinalized = null;
private Finalizer next, prev;
}

一模一樣的套路,Netty 為了保證在 DirectByteBuf 被 GC 之前,與其弱引用關(guān)聯(lián)的 DefaultResourceLeak 始終保持活躍不被 GC , 也需要在某一個(gè)地方來(lái)全局持有 DefaultResourceLeak 的強(qiáng)引用。
但和 Cleaner 與 Finalizer 不同的是,Netty 并沒(méi)有采用雙向鏈表的結(jié)構(gòu)來(lái)持有 DefaultResourceLeak 的強(qiáng)引用,而是選擇了 Set 結(jié)構(gòu)。
public class ResourceLeakDetector<T> {
private final Set<DefaultResourceLeak<?>> allLeaks =
Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());
}

之所以這里采用 Set 結(jié)構(gòu)就是為了實(shí)現(xiàn) “引用計(jì)數(shù)是否為 0” 的這層語(yǔ)義,那么如何實(shí)現(xiàn)呢 ?
Netty 在分配一個(gè) DirectByteBuf 的同時(shí)也會(huì)創(chuàng)建一個(gè) DefaultResourceLeak 對(duì)象來(lái)弱引用這個(gè) DirectByteBuf,隨后會(huì)將這個(gè) DefaultResourceLeak 對(duì)象放入到 allLeaks 集合中。
當(dāng)我們使用完 DirectByteBuf 并調(diào)用 release() 方法釋放其 Native Memory 的時(shí)候,如果它的引用計(jì)數(shù)為 0 ,那么 Netty 就會(huì)將它的 DefaultResourceLeak 對(duì)象從 allLeaks 集合中刪除。
如果我們使用完 DirectByteBuf 忘記調(diào)用 release() 方法,那么它的引用計(jì)數(shù)就會(huì)一直大于 0 ,同時(shí)也意味著它對(duì)應(yīng)的 DefaultResourceLeak 對(duì)象會(huì)一直停留在 allLeaks 集合中。
從另一個(gè)層面上來(lái)說(shuō),只要是停留在 allLeaks 集合中的 DefaultResourceLeak 對(duì)象,那么被其弱引用的 DirectByteBuf 的引用計(jì)數(shù)一定是大于 0 的。
當(dāng)這個(gè) DirectByteBuf 給 GC 回收之后,JVM 會(huì)將其對(duì)應(yīng)的 DefaultResourceLeak 插入到 _reference_pending_list 中,隨后 ReferenceHandler 線程會(huì)再一次將 DefaultResourceLeak 對(duì)象從 _reference_pending_list 中轉(zhuǎn)移到 ReferenceQueue 中。
當(dāng)某一個(gè)普通的 Java 線程在向 Netty 申請(qǐng) DirectByteBuf 的時(shí)候,這個(gè)申請(qǐng)內(nèi)存的線程就會(huì)順帶到 ReferenceQueue 中查看一下是否有 DefaultResourceLeak 對(duì)象,如果有,那么就證明被其弱引用的 DirectByteBuf 已經(jīng)被 GC 了。
緊接著,就會(huì)查看這個(gè) DefaultResourceLeak 對(duì)象是否仍然停留在 allLeaks 集合中 ,如果還在,那么就說(shuō)明 DirectByteBuf 背后的 Native Memory 仍然沒(méi)有被釋放,這樣一來(lái) Netty 就探測(cè)到了內(nèi)存泄露的發(fā)生。
好了,現(xiàn)在我們已經(jīng)清楚了 Netty 內(nèi)存泄露探測(cè)的核心設(shè)計(jì)原理,那么下面的內(nèi)容就很簡(jiǎn)單了,我們把視角在切換一下,從內(nèi)存泄露探測(cè)的內(nèi)部在轉(zhuǎn)換到外部,站在應(yīng)用的角度再來(lái)從整體上完整地看一下整個(gè)內(nèi)存泄露探測(cè)機(jī)制。
2. Netty 的內(nèi)存泄露探測(cè)機(jī)制
從總體上來(lái)講,觸發(fā)內(nèi)存泄露的探測(cè)需要同時(shí)滿足以下五個(gè)條件:
應(yīng)用必須開(kāi)啟內(nèi)存泄露探測(cè)功能。
必須要等到 ByteBuf 被 GC 之后,內(nèi)存泄露才能探測(cè)的到,如果 GC 一直沒(méi)有觸發(fā),那么即使是 ByteBuf 沒(méi)有任何強(qiáng)引用或者軟引用了,內(nèi)存泄露的探測(cè)也將無(wú)從談起。
當(dāng) GC 發(fā)生之后,必須是要等到下一次分配內(nèi)存的時(shí)候,才會(huì)觸發(fā)內(nèi)存泄露的探測(cè)。如果沒(méi)有內(nèi)存申請(qǐng)的行為發(fā)生,那么內(nèi)存泄露的探測(cè)也不會(huì)發(fā)生。
Netty 并不會(huì)探測(cè)每一個(gè) ByteBuf 的泄露情況,而是根據(jù)一定的采樣間隔,進(jìn)行采樣探測(cè)。所以要想觸發(fā)內(nèi)存泄露的探測(cè),還需要達(dá)到一定的采樣間隔。
應(yīng)用的日志級(jí)別必須開(kāi)啟
Error級(jí)別,因?yàn)閮?nèi)存泄露的報(bào)告,Netty 是以Error級(jí)別的日志打印出來(lái)的,如果日志級(jí)別在Error以下,那么內(nèi)存泄露的報(bào)告則無(wú)法輸出。
除此之外,Netty 還為內(nèi)存泄露的探測(cè)設(shè)置了四種級(jí)別:
public enum Level {
DISABLED,
SIMPLE,
ADVANCED,
PARANOID;
}
我們可以通過(guò) JVM 參數(shù) -Dio.netty.leakDetection.level 為應(yīng)用設(shè)置不同的探測(cè)級(jí)別,其中 DISABLED 表示禁用內(nèi)存泄露探測(cè),因?yàn)閮?nèi)存泄露探測(cè)開(kāi)啟之后,應(yīng)用對(duì)于 ByteBuf 的訪問(wèn)鏈路會(huì)變長(zhǎng),而且 Netty 需要記錄 ByteBuf 的創(chuàng)建位置堆棧,以及訪問(wèn)鏈路堆棧,這樣在內(nèi)存泄露報(bào)告中,我們才可以清楚的知道泄露的 ByteBuf 是在哪里創(chuàng)建的,又是在哪里泄露的,它的訪問(wèn)路徑有哪些。

而報(bào)告中的每一個(gè)堆棧在內(nèi)存中占用 2K 大小,所以內(nèi)存消耗還是非??捎^的,所以筆者一般建議在生產(chǎn)環(huán)境中,要將 Netty 的內(nèi)存泄露探測(cè)關(guān)閉掉。而在測(cè)試環(huán)境中,則仍然開(kāi)啟內(nèi)存泄露探測(cè)。
當(dāng)內(nèi)存泄露探測(cè)開(kāi)啟之后,Netty 為我們提供了三種不同的探測(cè)級(jí)別,級(jí)別越高,消耗越大,信息也越詳細(xì)。第一種探測(cè)級(jí)別是 SIMPLE , 這也是 Netty 默認(rèn)的探測(cè)級(jí)別。
SIMPLE 級(jí)別下,Netty 并不會(huì)探測(cè)每一個(gè) ByteBuf 的泄露情況,而是選擇進(jìn)行采樣探測(cè),默認(rèn)的采樣間隔是 128 。
public class ResourceLeakDetector<T> {
// 采樣間隔,默認(rèn) 128
static final int SAMPLING_INTERVAL;
private static final String PROP_SAMPLING_INTERVAL = "io.netty.leakDetection.samplingInterval";
private static final int DEFAULT_SAMPLING_INTERVAL = 128;
SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL);
}
我們可以通過(guò) JVM 參數(shù) -Dio.netty.leakDetection.samplingInterval 來(lái)設(shè)置內(nèi)存泄露探測(cè)的采樣間隔。那么 Netty 如何根據(jù)這個(gè)采樣間隔來(lái)決定到底為哪一個(gè)具體的 ByteBuf 探測(cè)內(nèi)存泄露呢 ?
事實(shí)上,這個(gè)探測(cè)頻率的實(shí)現(xiàn)也很簡(jiǎn)單,在每一次內(nèi)存申請(qǐng)之后,Netty 都會(huì)生成 [ 0 , samplingInterval ) 之間的一個(gè)隨機(jī)數(shù),如果這個(gè)隨機(jī)數(shù)是 0 ,Netty 將會(huì)為本次申請(qǐng)到的 ByteBuf 進(jìn)行內(nèi)存泄露探測(cè),如果這個(gè)隨機(jī)數(shù)不為 0 ,Netty 將放棄探測(cè)。
PlatformDependent.threadLocalRandom().nextInt(samplingInterval) == 0
從效果上來(lái)看,就是每申請(qǐng) samplingInterval 個(gè) ByteBuf , Netty 就會(huì)觸發(fā)一次內(nèi)存泄露的探測(cè)。
除了受到這個(gè)采用頻率的限制之外,SIMPLE 級(jí)別下的內(nèi)存泄露報(bào)告信息是最少的,只會(huì)包含 ByteBuf 的創(chuàng)建位置,后面針對(duì) ByteBuf 的訪問(wèn)堆棧信息 Netty 就不會(huì)跟蹤了,也就是日志中的 Recent access records: 信息,在 SIMPLE 級(jí)別下是沒(méi)有的。

ADVANCED 級(jí)別和 SIMPLE 級(jí)別一樣,在這兩種探測(cè)級(jí)別下,Netty 都會(huì)選擇進(jìn)行采樣探測(cè),而不是為每一個(gè) ByteBuf 進(jìn)行探測(cè),同樣都會(huì)受到采樣頻率的限制。
那么 ADVANCED 究竟比 SIMPLE 高級(jí)在哪里呢 ?SIMPLE 級(jí)別只會(huì)報(bào)告泄露的 ByteBuf 是在哪里創(chuàng)建的, ADVANCED 級(jí)別則除了泄露 ByteBuf 的創(chuàng)建位置之外,還會(huì)跟蹤 ByteBuf 的每一次訪問(wèn)堆棧,也就是下面內(nèi)存泄露報(bào)告日志中的 Recent access records 相關(guān)信息。

前面筆者也提過(guò),追蹤 ByteBuf 的訪問(wèn)堆棧是需要消耗非??捎^的內(nèi)存的,對(duì)于 ByteBuf 的每一次訪問(wèn)堆棧,如果要記錄的話,每個(gè)堆棧占用 2K 的內(nèi)存,堆棧信息 Netty 會(huì)記錄在一個(gè) TraceRecord 結(jié)構(gòu)中。
如果一個(gè) ByteBuf 被訪問(wèn)了多次,那么就會(huì)對(duì)應(yīng)多個(gè) TraceRecord 結(jié)構(gòu),ByteBuf 的這些 TraceRecord , 被 Netty 組織在對(duì)應(yīng) DefaultResourceLeak 里的一個(gè)棧結(jié)構(gòu)中,位于棧底的 TraceRecord 記錄的是 ByteBuf 的創(chuàng)建堆棧,位于棧頂?shù)?TraceRecord 記錄的是 ByteBuf 最近一次被訪問(wèn)的堆棧。
private static final class DefaultResourceLeak<T> {
// 棧頂指針
private volatile TraceRecord head; // 棧結(jié)構(gòu),存放對(duì)應(yīng) ByteBuf 的訪問(wèn)堆棧
}
private static class TraceRecord extends Throwable {
// 棧底
private static final TraceRecord BOTTOM = new TraceRecord()
}
由于每個(gè) TraceRecord 中記錄的訪問(wèn)堆棧信息占用 2K 的內(nèi)存,因此無(wú)論在什么探測(cè)級(jí)別下,Netty 都不可能為 ByteBuf 的每一次訪問(wèn)都記錄下堆棧信息,所以要對(duì) DefaultResourceLeak 棧中 TraceRecord 的個(gè)數(shù)進(jìn)行限制。默認(rèn)棧中的 TraceRecord 最大個(gè)數(shù)為 4 , 我們可以通過(guò) -Dio.netty.leakDetection.targetRecords 參數(shù)進(jìn)行調(diào)節(jié)。
public class ResourceLeakDetector<T> {
// ByteBuf 訪問(wèn)堆棧記錄個(gè)數(shù)限制,默認(rèn)為 4
private static final int TARGET_RECORDS;
private static final String PROP_TARGET_RECORDS = "io.netty.leakDetection.targetRecords";
private static final int DEFAULT_TARGET_RECORDS = 4;
TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
}
但更加準(zhǔn)確的說(shuō),targetRecords 只是對(duì)棧中的 TraceRecord 個(gè)數(shù)進(jìn)行限制,避免無(wú)限的增長(zhǎng),但不會(huì)限制死。事實(shí)上, 棧中 TraceRecord 個(gè)數(shù)有一定的概率會(huì)超過(guò) targetRecords 的限制。
比如,默認(rèn)情況下 targetRecords 的值為 4 ,如果我們將棧中 TraceRecord 個(gè)數(shù)限制成 4 個(gè)的話,當(dāng)一個(gè) ByteBuf 的訪問(wèn)鏈路很長(zhǎng)的話,那么棧中就只能記錄前三個(gè)最遠(yuǎn)的 TraceRecord 和一個(gè)最近的 TraceRecord。中間的訪問(wèn)堆棧就丟失了。這樣不利于我們排查 ByteBuf 的完整泄露路徑。
事實(shí)上 targetRecords 的真正語(yǔ)義是,當(dāng) ByteBuf 的訪問(wèn)堆棧記錄 TraceRecord 個(gè)數(shù)達(dá)到 targetRecords 的限定時(shí),Netty 會(huì)根據(jù)一定的概率來(lái)丟棄當(dāng)前棧頂 TraceRecord,并將新的 TraceRecord 作為棧頂。這個(gè)丟棄的概率是非常高的,從而避免了 TraceRecord 個(gè)數(shù)瘋狂地增長(zhǎng)。
但如果恰好命中了不丟棄的概率(非常低),那么原來(lái)?xiàng)m數(shù)?TraceRecord 將不會(huì)被丟棄而是繼續(xù)保留在棧中,新的 TraceRecord 作為棧頂加入到棧中,這樣一來(lái)?xiàng)V?TraceRecord 個(gè)數(shù)就超過(guò)了 targetRecords 的限制。但是可以盡可能多的保留 ByteBuf 中間的訪問(wèn)堆棧記錄。使得 ByteBuf 的泄露路徑更加完整一些。
PARANOID 是 Netty 內(nèi)存泄露探測(cè)的最高級(jí)別,信息最全,消耗也最大,它在 ADVANCED 的基礎(chǔ)之上,繞開(kāi)了采樣頻率的限制,會(huì)對(duì)每一個(gè) ByteBuf 進(jìn)行詳細(xì)地泄露探測(cè)。一般用于需要在測(cè)試環(huán)境定位緊急的內(nèi)存泄露問(wèn)題才會(huì)開(kāi)啟。
3. 內(nèi)存泄露探測(cè)相關(guān)的設(shè)計(jì)模型
現(xiàn)在我們已經(jīng)清楚了內(nèi)存泄露探測(cè)的設(shè)計(jì)原理以及相關(guān)應(yīng)用,那么在本小節(jié)中就該正式介紹實(shí)現(xiàn)細(xì)節(jié)了,Netty 一共設(shè)計(jì)了 4 種探測(cè)模型,不同的模型封裝不同的探測(cè)職責(zé)。
3.1 ResourceLeakDetector
首先第一個(gè)模型是 ResourceLeakDetector 。顧名思義,它主要負(fù)責(zé)內(nèi)存泄露的探測(cè),第一小節(jié)中介紹的原理實(shí)現(xiàn),就是在這個(gè)模型中完成的。
public class ResourceLeakDetector<T> {
// 探測(cè)級(jí)別
private static Level level;
// 未被釋放的 ByteBuf 對(duì)應(yīng)的弱引用 DefaultResourceLeak 集合
private final Set<DefaultResourceLeak<?>> allLeaks =
Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());
// 用于接收 ByteBuf 被回收的通知
private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
// 探測(cè)的資源類型,這里是 ByteBuf
private final String resourceType;
// 采樣間隔
private final int samplingInterval;
// 內(nèi)存泄露監(jiān)聽(tīng)器,一旦探測(cè)到內(nèi)存泄露,Netty 就會(huì)回調(diào) LeakListener
private volatile LeakListener leakListener;
}
ResourceLeakDetector 中封裝了內(nèi)存泄露探測(cè)所需要的所有信息,其中最重要的就是 allLeaks 和 refQueue 這兩個(gè)集合,allLeaks 主要用于保存所有未被釋放的 ByteBuf 對(duì)應(yīng)的弱引用 DefaultResourceLeak,在 ByteBuf 被創(chuàng)建之后,Netty 就會(huì)為其創(chuàng)建一個(gè) DefaultResourceLeak 實(shí)例來(lái)弱引用 ByteBuf,同時(shí)這個(gè) DefaultResourceLeak 會(huì)被添加到這里的 allLeaks 中。
如果應(yīng)用程序及時(shí)的釋放了 ByteBuf , 那么對(duì)應(yīng)的 DefaultResourceLeak 也會(huì)從 allLeaks 中刪除,如果 ByteBuf 被 GC 之后,其對(duì)應(yīng)的 DefaultResourceLeak 仍然停留在 allLeaks 中,那么就說(shuō)明該 ByteBuf 發(fā)生泄露了。

refQueue 主要用于收集被 GC 的 ByteBuf 對(duì)應(yīng)的弱引用 DefaultResourceLeak,當(dāng)一個(gè) ByteBuf 被 GC 之后,那么其對(duì)應(yīng)的 DefaultResourceLeak 就會(huì)被 JVM 放入到一個(gè)內(nèi)部的 _reference_pending_list 中,隨后 ReferenceHandler 線程被喚醒,將 DefaultResourceLeak 從 _reference_pending_list 中轉(zhuǎn)移到這里的 refQueue。

后續(xù) ResourceLeakDetector 就會(huì)從 refQueue 中將 DefaultResourceLeak 摘下,然后檢查這個(gè) DefaultResourceLeak 是否仍然停留在 allLeaks 集合中。如果存在,就說(shuō)明對(duì)應(yīng)的 ByteBuf 發(fā)生了泄露,最后將泄露路徑以 ERROR 級(jí)別的日志打印出來(lái)。
除此之外,Netty 還提供了一個(gè)內(nèi)存泄露監(jiān)聽(tīng)器,讓我們可以在內(nèi)存泄露發(fā)生之后實(shí)現(xiàn)自主的處理邏輯。
public interface LeakListener {
/**
* Will be called once a leak is detected.
*/
void onLeak(String resourceType, String records);
}
我們可以通過(guò) ByteBufUtil.setLeakListener 方法來(lái)向 ResourceLeakDetector 注冊(cè) LeakListener。
public final class ByteBufUtil {
public static void setLeakListener(ResourceLeakDetector.LeakListener leakListener) {
AbstractByteBuf.leakDetector.setLeakListener(leakListener);
}
}
一旦 ResourceLeakDetector 探測(cè)到內(nèi)存泄露的發(fā)生,Netty 就會(huì)回調(diào)我們注冊(cè)的 LeakListener。
Netty 在全局范圍內(nèi)只會(huì)有一個(gè) ResourceLeakDetector 實(shí)例,被 AbstractByteBuf 的靜態(tài)字段 leakDetector 所引用。
public abstract class AbstractByteBuf extends ByteBuf {
// 全局 ResourceLeakDetector 實(shí)例
static final ResourceLeakDetector<ByteBuf> leakDetector =
ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);
}
內(nèi)存泄露探測(cè)器的默認(rèn)實(shí)現(xiàn)是 ResourceLeakDetector,但我們也可以自定義實(shí)現(xiàn)內(nèi)存泄露探測(cè)器,只需要繼承 ResourceLeakDetector 類,并覆蓋實(shí)現(xiàn)相關(guān)的核心探測(cè)方法,最后通過(guò) JVM 參數(shù) -Dio.netty.customResourceLeakDetector={className} 指定即可。
ResourceLeakDetector 最核心的方法莫過(guò)于 track(T obj) 和 reportLeak() 這兩個(gè)方法。
public class ResourceLeakDetector<T> {
public final ResourceLeakTracker<T> track(T obj) {
return track0(obj, false);
}
// 采樣頻率,默認(rèn) 128
private final int samplingInterval;
// 對(duì) obj 進(jìn)行資源泄露的探測(cè)
// force 表示是否強(qiáng)制探測(cè)
private DefaultResourceLeak track0(T obj, boolean force) {
Level level = ResourceLeakDetector.level;
if (force ||
level == Level.PARANOID ||
(level != Level.DISABLED && PlatformDependent.threadLocalRandom().nextInt(samplingInterval) == 0)) {
// 觸發(fā)內(nèi)存泄露探測(cè),如果發(fā)生內(nèi)存泄露,則在日志中 report
reportLeak();
// 創(chuàng)建 ByteBuf (obj) 對(duì)應(yīng)的弱引用 DefaultResourceLeak
// ResourceLeakDetector 中的全局 refQueue , allLeaks 會(huì)在這里注冊(cè)進(jìn)去
return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
}
return null;
}
}
其中 track 方法用于觸發(fā)內(nèi)存泄露的探測(cè),這里是對(duì)第二小節(jié)中的內(nèi)容實(shí)現(xiàn),如果我們?cè)O(shè)置的內(nèi)存泄露探測(cè)級(jí)別為 PARANOID , 那么 Netty 就會(huì)對(duì)系統(tǒng)中所有的 ByteBuf 進(jìn)行全量探測(cè),內(nèi)存泄露發(fā)生之后的報(bào)告日志也會(huì)包含詳細(xì)的泄露堆棧路徑。
如果內(nèi)存泄露探測(cè)級(jí)別為 SIMPLE 或者 ADVANCED , 那么 Netty 就會(huì)對(duì)系統(tǒng)中的 ByteBuf 進(jìn)行采樣探測(cè),采樣間隔 SAMPLING_INTERVAL = 128 , 我們可以通過(guò) JVM 參數(shù) -Dio.netty.leakDetection.samplingInterval 進(jìn)行設(shè)置。
具體的采樣邏輯是,Netty 會(huì)生成 [ 0 , samplingInterval ) 之間的一個(gè)隨機(jī)數(shù),如果這個(gè)隨機(jī)數(shù)是 0 ,那么就進(jìn)行內(nèi)存泄露探測(cè),如果這個(gè)隨機(jī)數(shù)不為 0 ,則放棄探測(cè)。從效果上來(lái)看,就是每申請(qǐng) samplingInterval 個(gè) ByteBuf , Netty 就會(huì)觸發(fā)一次內(nèi)存泄露的探測(cè)。
PlatformDependent.threadLocalRandom().nextInt(samplingInterval) == 0
當(dāng)符合內(nèi)存泄露的探測(cè)條件之后,Netty 將會(huì)在 reportLeak() 方法中進(jìn)行內(nèi)存泄露的探測(cè),如果有內(nèi)存泄露的發(fā)生,那么就將泄露的 ByteBuf 相關(guān)訪問(wèn)路徑以 ERROR 的日志級(jí)別打印出來(lái)。
既然內(nèi)存泄露的日志級(jí)別是 ERROR , 那么在進(jìn)行內(nèi)存泄露探測(cè)之前,我們首先必須檢查一下用戶是否開(kāi)啟了 ERROR 日志級(jí)別。
protected boolean needReport() {
return logger.isErrorEnabled();
}
如果用戶選擇的日志級(jí)別比較低,那么即使發(fā)生了內(nèi)存泄露,相關(guān)的 ERROR 日志也不會(huì)打印,這種情況下內(nèi)存泄露的探測(cè)也就沒(méi)必要進(jìn)行了。Netty 會(huì)調(diào)用 clearRefQueue() 方法,將 refQueue 中收集到的所有 DefaultResourceLeak 實(shí)例清空,并且將 DefaultResourceLeak 從 allLeaks 集合中刪除。
private void clearRefQueue() {
for (;;) {
// 清空 refQueue
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
// 將 DefaultResourceLeak 從 allLeaks 集合中刪除。
ref.dispose();
}
}
如果用戶的日志級(jí)別選擇的是 ERROR , Netty 就會(huì)繼續(xù)后面的內(nèi)存泄露探測(cè)流程,首先一個(gè) ByteBuf 如果被 GC 回收的話,那么與其弱引用關(guān)聯(lián)的 DefaultResourceLeak 就會(huì)被 ReferenceHandler 線程轉(zhuǎn)移到 refQueue 中。
也就是說(shuō)當(dāng)前 refQueue 中保留的所有 DefaultResourceLeak 其對(duì)應(yīng)的 ByteBuf 已經(jīng)被 GC 回收了,而內(nèi)存泄露探測(cè)針對(duì)地就是這些被回收的 ByteBuf。
Netty 會(huì)從 refQueue 中將這些收集到的 DefaultResourceLeak 挨個(gè)摘下。
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
然后調(diào)用 dispose() 方法檢查 DefaultResourceLeak 實(shí)例是否仍然停留在 allLeaks 集合中。
boolean dispose() {
// 斷開(kāi) DefaultResourceLeak 與 ByteBuf 的弱引用關(guān)聯(lián)
clear();
// 檢查 DefaultResourceLeak 實(shí)例是否仍然存在于 allLeaks 集合中。
return allLeaks.remove(this);
}
如果仍然停留在 allLeaks 中,那么就說(shuō)明該 DefaultResourceLeak 實(shí)例對(duì)應(yīng)的 ByteBuf 出現(xiàn)內(nèi)存泄露了。在探測(cè)到內(nèi)存泄露發(fā)生之后,調(diào)用 getReportAndClearRecords() 方法獲取 ByteBuf 相關(guān)的訪問(wèn)堆棧路徑,然后通過(guò) reportTracedLeak 方法將 ByteBuf 的泄露路徑以 ERROR 級(jí)別的日志打印出來(lái),最后回調(diào)內(nèi)存泄露監(jiān)聽(tīng)器 LeakListener。
// resourceType 為需要探測(cè)的資源類型,這里是 ByteBuf
// records 是發(fā)生內(nèi)存泄露的 ByteBuf 相關(guān)的訪問(wèn)堆棧
protected void reportTracedLeak(String resourceType, String records) {
logger.error(
"LEAK: {}.release() was not called before it's garbage-collected. " +
"See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
resourceType, records);
}
reportLeak() 方法的實(shí)現(xiàn)邏輯正是筆者在第一小節(jié)中介紹的所有內(nèi)容:
private void reportLeak() {
// 日志級(jí)別必須是 Error 級(jí)別
if (!needReport()) {
clearRefQueue();
return;
}
// Detect and report previous leaks.
for (;;) {
// 對(duì)應(yīng)的 ByteBuf 必須已經(jīng)被 GC 回收,才會(huì)觸發(fā)內(nèi)存泄露的探測(cè)
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
// 檢查 ByteBuf 對(duì)應(yīng)的 DefaultResourceLeak 是否仍然停留在 allLeaks 集合中
if (!ref.dispose()) {
// 如果不存在,則說(shuō)明 ByteBuf 已經(jīng)被及時(shí)的釋放了,不存在內(nèi)存泄露
continue;
}
// 當(dāng)探測(cè)到 ByteBuf 發(fā)生內(nèi)存泄露之后,這里會(huì)獲取 ByteBuf 相關(guān)的訪問(wèn)堆棧
String records = ref.getReportAndClearRecords();
if (reportedLeaks.add(records)) { // 去重泄露日志
// 打印泄露的堆棧路徑
if (records.isEmpty()) {
reportUntracedLeak(resourceType);
} else {
reportTracedLeak(resourceType, records);
}
// 回調(diào) LeakListener
LeakListener listener = leakListener;
if (listener != null) {
listener.onLeak(resourceType, records);
}
}
}
}
3.2 ResourceLeakTracker
上一小節(jié)介紹的 ResourceLeakDetector 只是負(fù)責(zé)內(nèi)存泄露的探測(cè),但如果探測(cè)到了內(nèi)存泄露,相關(guān)的泄露路徑信息從哪里來(lái)的呢 ?Netty 是如何收集的 ?這就引入了第二個(gè)探測(cè)模型 —— ResourceLeakTracker。
Netty 對(duì) ResourceLeakTracker 的默認(rèn)實(shí)現(xiàn)是 DefaultResourceLeak,它是一個(gè) WeakReference ,被 Netty 用來(lái)弱引用關(guān)聯(lián) ByteBuf , 目的是接收 ByteBuf 被 GC 回收的通知,從而可以判斷是否有內(nèi)存泄露的情況發(fā)生。

除此之外,ResourceLeakTracker 承擔(dān)的另一個(gè)重要職責(zé)就是負(fù)責(zé)收集 ByteBuf 的訪問(wèn)鏈路堆棧,一旦 ByteBuf 發(fā)生泄露,ResourceLeakDetector 就會(huì)從 ResourceLeakTracker 中獲取相關(guān)的泄露堆棧 —— getReportAndClearRecords() 方法,并在日志中打印出來(lái)。
每一條 ByteBuf 相關(guān)的訪問(wèn)鏈路堆棧信息,Netty 用一個(gè) TraceRecord 結(jié)構(gòu)來(lái)封裝,而一個(gè) ByteBuf 會(huì)有多條訪問(wèn)鏈路,那么在它的 ResourceLeakTracker 結(jié)構(gòu)中就對(duì)應(yīng)多個(gè) TraceRecords,這些 TraceRecords 被 Netty 組織在一個(gè)棧的結(jié)構(gòu)中。

private static final class DefaultResourceLeak<T>
extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
// 棧頂指針
private volatile TraceRecord head;
// 棧中被丟棄的 TraceRecord 個(gè)數(shù)
private volatile int droppedRecords;
// 指向 ResourceLeakDetector 中的全局 allLeaks
private final Set<DefaultResourceLeak<?>> allLeaks;
// 被追蹤探測(cè)的 Bytebuf 的 hash 值
private final int trackedHash;
}
當(dāng) Netty 新分配一個(gè) ByteBuf 之后,如果符合 ResourceLeakDetector.track 中的探測(cè)條件,那么就會(huì)創(chuàng)建一個(gè) DefaultResourceLeak 來(lái)弱引用這個(gè) ByteBuf。同時(shí)將這個(gè) DefaultResourceLeak 加入到 allLeaks 集合中,這里正是判斷一個(gè) ByteBuf 是否發(fā)生內(nèi)存泄露的關(guān)鍵依據(jù)。
無(wú)論什么樣的探測(cè)級(jí)別,DefaultResourceLeak 都會(huì)至少保留一個(gè) TraceRecord , 這個(gè) TraceRecord 用于保存 ByteBuf 的創(chuàng)建位置堆棧,在構(gòu)建 DefaultResourceLeak 的時(shí)候會(huì)被加入到棧底。

DefaultResourceLeak(
Object referent,
ReferenceQueue<Object> refQueue,
Set<DefaultResourceLeak<?>> allLeaks,
Object initialHint) {
// 弱引用關(guān)聯(lián) ByteBuf (referent)
// 注冊(cè) refQueue
super(referent, refQueue);
// 保存 Bytebuf 的 hash 值
trackedHash = System.identityHashCode(referent);
// 加入到 allLeaks 中,如果 ByteBuf 被回收之后,DefaultResourceLeak 仍然停留在 allLeaks,則表示發(fā)生內(nèi)存泄露。
allLeaks.add(this);
// 創(chuàng)建第一個(gè) TraceRecord,記錄 ByteBuf 的創(chuàng)建位置堆棧,保存在棧底
headUpdater.set(this, initialHint == null ?
new TraceRecord(TraceRecord.BOTTOM) : new TraceRecord(TraceRecord.BOTTOM, initialHint));
this.allLeaks = allLeaks;
}
另外我們可以通過(guò) record 相關(guān)方法,來(lái)向 DefaultResourceLeak 添加 ByteBuf 的當(dāng)前訪問(wèn)堆棧。
@Override
public void record() {
record0(null);
}
@Override
public void record(Object hint) {
record0(hint);
}
通過(guò) record(Object hint) 添加的堆棧,會(huì)在泄露日志中出現(xiàn)我們自定義的提示信息。

而通過(guò) record() 添加的堆棧,在泄露日志中就沒(méi)有這個(gè)提示信息。

向 DefaultResourceLeak 添加新 TraceRecord 的邏輯也很簡(jiǎn)單,就是將 ByteBuf 當(dāng)前最新的訪問(wèn)堆棧信息 —— TraceRecord 入棧即可。但也不能無(wú)限制的向棧中添加 TraceRecord。
第二小節(jié)筆者介紹過(guò),每個(gè) TraceRecord 中記錄的訪問(wèn)堆棧信息占用 2K 的內(nèi)存,Netty 不可能為 ByteBuf 的每一次訪問(wèn)都記錄下堆棧信息,所以 DefaultResourceLeak 棧中的個(gè)數(shù)會(huì)受到 TARGET_RECORDS 的限制,默認(rèn)為 4 , 我們可以通過(guò) -Dio.netty.leakDetection.targetRecords 參數(shù)進(jìn)行調(diào)節(jié)。
當(dāng) DefaultResourceLeak 棧中記錄的 TraceRecord 個(gè)數(shù)達(dá)到 TARGET_RECORDS 的限定時(shí),Netty 會(huì)根據(jù)一定的概率(比較高)來(lái)丟棄當(dāng)前棧頂 TraceRecord,并將新的 TraceRecord 作為棧頂。從而避免了 TraceRecord 個(gè)數(shù)瘋狂地增長(zhǎng)。
但如果恰好命中了不丟棄的概率(非常低),那么原來(lái)?xiàng)m數(shù)?TraceRecord 將不會(huì)丟棄而是繼續(xù)保留在棧中,新的 TraceRecord 作為棧頂加入到棧中,這樣一來(lái)?xiàng)V?TraceRecord 個(gè)數(shù)就超過(guò)了 TARGET_RECORDS 的限制。但是可以盡可能多的保留 ByteBuf 中間的訪問(wèn)堆棧記錄。使得 ByteBuf 的泄露路徑更加完整一些。
丟棄概率的計(jì)算邏輯也很簡(jiǎn)單,Netty 仍然是通過(guò)計(jì)算一個(gè)[ 0 , 1 << backOffFactor ) 區(qū)間的隨機(jī)數(shù),如果這個(gè)隨機(jī)數(shù)不為 0 ,那么就將當(dāng)前的棧頂元素丟棄,這么看來(lái),當(dāng) DefaultResourceLeak 棧中 TraceRecord 個(gè)數(shù)達(dá)到 TARGET_RECORDS 的限定,如果繼續(xù)添加 TraceRecord,那么棧頂元素被丟棄的概率還是非常高的。
// numElements 為當(dāng)前棧中的 TraceRecord 個(gè)數(shù)
final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30)
dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0
TraceRecord 完整的入棧邏輯如下:
private void record0(Object hint) {
if (TARGET_RECORDS > 0) {
TraceRecord oldHead;
TraceRecord prevHead;
TraceRecord newHead;
boolean dropped;
do {
// 獲取棧頂 TraceRecord,也就是 ByteBuf 最近一次的訪問(wèn)堆棧
if ((prevHead = oldHead = headUpdater.get(this)) == null) {
// 棧頂為 null ,表示 ByteBuf 已經(jīng)被釋放,對(duì)應(yīng)的泄露探測(cè)已經(jīng)關(guān)閉。
return;
}
// 獲取當(dāng)前棧中的 TraceRecord 個(gè)數(shù)
final int numElements = oldHead.pos + 1;
// 如果達(dá)到 TARGET_RECORDS 的限制,就開(kāi)始概率性的丟棄當(dāng)前棧頂
// 然后用新的 TraceRecord 作為棧頂
if (numElements >= TARGET_RECORDS) {
final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
// numElements 超出 TARGET_RECORDS 的限制越多,當(dāng)前棧頂就越容易被 drop
if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
// 命中丟棄的概率,則將當(dāng)前棧頂 TraceRecord 丟棄
prevHead = oldHead.next;
}
} else {
// 保留當(dāng)前棧頂,這樣棧中的 TraceRecord 個(gè)數(shù)就會(huì)超過(guò) TARGET_RECORDS 的限制
// 但 ByteBuf 中間的訪問(wèn)鏈路堆棧就會(huì)被概率性的保留下來(lái)
dropped = false;
}
// 創(chuàng)建的新的 TraceRecord(記錄 ByteBuf 的當(dāng)前訪問(wèn)堆棧)
// 并作為新的棧頂元素
newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
} while (!headUpdater.compareAndSet(this, oldHead, newHead));
if (dropped) {
// 統(tǒng)計(jì)被丟棄的 TraceRecord 個(gè)數(shù)
droppedRecordsUpdater.incrementAndGet(this);
}
}
}
好了,現(xiàn)在我們已經(jīng)清楚了,Netty 如何通過(guò) DefaultResourceLeak 來(lái)收集 ByteBuf 相關(guān)的訪問(wèn)鏈路堆棧信息,那么當(dāng)這個(gè) ByteBuf 發(fā)生內(nèi)存泄露之后,Netty 又是如何生成相關(guān)的泄露堆棧呢 ?
這就要依靠 DefaultResourceLeak 中的這個(gè) TraceRecord 棧結(jié)構(gòu),棧頂 TraceRecord 永遠(yuǎn)保存的是 ByteBuf 最近一次的訪問(wèn)堆棧,棧底 TraceRecord 永遠(yuǎn)保存的是 ByteBuf 起始創(chuàng)建位置堆棧,中間的 TraceRecord 記錄的是 ByteBuf 的訪問(wèn)鏈路堆棧。

ByteBuf 的泄露堆棧是從棧頂?shù)?TraceRecord 開(kāi)始打印,一直到棧底 TraceRecord,也就是由近及遠(yuǎn)的輸出 ByteBuf 的泄露路徑。
String getReportAndClearRecords() {
// 獲取棧頂 TraceRecord
TraceRecord oldHead = headUpdater.getAndSet(this, null);
// 由近及遠(yuǎn)的輸出 ByteBuf 相關(guān)的 TraceRecords
return generateReport(oldHead);
}
首先 Netty 會(huì)打印一行 Recent access records: , 然后每一個(gè) TraceRecord 在日志中都有一個(gè) # 字編號(hào),棧頂?shù)?TraceRecord 編號(hào)為 #1 , 后面依次遞增,棧底的 TraceRecord 由于記錄的是創(chuàng)建位置堆棧,Netty 在日志中會(huì)提示 Created at:。

private String generateReport(TraceRecord oldHead) {
// 當(dāng)前 DefaultResourceLeak 棧中一共有多少個(gè) TraceRecord
int present = oldHead.pos + 1;
// 每個(gè) TraceRecord 分配 2K 大小的內(nèi)存
StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
buf.append("Recent access records: ").append(NEWLINE);
int i = 1;
// 防重集合
Set<String> seen = new HashSet<String>(present);
// 從棧頂開(kāi)始生成泄露堆棧
for (; oldHead != TraceRecord.BOTTOM; oldHead = oldHead.next) {
// 獲取 TraceRecord 記錄的堆棧信息
String s = oldHead.toString();
if (seen.add(s)) {
if (oldHead.next == TraceRecord.BOTTOM) {
// 棧底 TraceRecord 記錄了 Buffer 的創(chuàng)建位置
buf.append("Created at:").append(NEWLINE).append(s);
} else {
buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
}
} else {
// 重復(fù)的 TraceRecord 個(gè)數(shù)
duped++;
}
}
// 生成泄露堆棧,并返回
buf.setLength(buf.length() - NEWLINE.length());
return buf.toString();
}
3.3 TraceRecord
上述內(nèi)存泄露日志中出現(xiàn)的每一條訪問(wèn)堆棧是如何生成的呢 ? 這就引入了第三個(gè)模型 —— TraceRecord , 該模型在內(nèi)存泄露探測(cè)中用于記錄 ByteBuf 某次的訪問(wèn)堆棧。實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單,只需要繼承 Throwable 即可,這樣在每次創(chuàng)建 TraceRecord 的時(shí)候,就會(huì)自動(dòng)生成 ByteBuf 當(dāng)前的訪問(wèn)堆棧。
由于 TraceRecord 在 DefaultResourceLeak 中是被組織在一個(gè)棧結(jié)構(gòu)中,所以它的 next 指針指向棧中下一個(gè) TraceRecord, pos 用于標(biāo)識(shí)當(dāng)前 TraceRecord 在棧中的位置,整個(gè)結(jié)構(gòu)比較簡(jiǎn)單明了。
private static class TraceRecord extends Throwable {
// 空實(shí)現(xiàn),用來(lái)標(biāo)識(shí)棧底位置
private static final TraceRecord BOTTOM = new TraceRecord() {
@Override
public Throwable fillInStackTrace() {
return this;
}
};
// 出現(xiàn)在日志中的自定義 Hint 提示信息
private final String hintString;
// 棧中下一個(gè) TraceRecord
private final TraceRecord next;
// 當(dāng)前 TraceRecord 在棧中的位置
private final int pos;
}

TraceRecord 的 toString() 方法用于生成其中記錄的堆棧信息,實(shí)現(xiàn)也很簡(jiǎn)單,就是直接打印 Throwable 中的堆棧即可。
@Override
public String toString() {
// 每個(gè) TraceRecord 堆棧信息占用 2K 內(nèi)存
StringBuilder buf = new StringBuilder(2048);
if (hintString != null) {
// 日志中顯示我們自定義的提示信息 tHint
buf.append("\tHint: ").append(hintString).append(NEWLINE);
}
// 獲取 TraceRecord 記錄的堆棧信息
StackTraceElement[] array = getStackTrace();
// Skip the first three elements.
out: for (int i = 3; i < array.length; i++) {
StackTraceElement element = array[i];
....... 清理一些沒(méi)用的堆棧信息 ......
// 生成有效的堆棧信息
buf.append('\t');
buf.append(element.toString());
buf.append(NEWLINE);
}
return buf.toString();
}
3.4 LeakAwareByteBuf
關(guān)于內(nèi)存泄露探測(cè)所有的核心設(shè)計(jì),到這里筆者就為大家介紹完了,當(dāng)我們清楚了這些背景之后,在回頭來(lái)看筆者在文章開(kāi)始處提出的疑問(wèn),是不是多多少少會(huì)有一些感覺(jué)了 ?
在 Netty 每次分配內(nèi)存的時(shí)候,都會(huì)觸發(fā)內(nèi)存泄露的采樣探測(cè),如果命中采樣概率,則會(huì)對(duì)本次分配的 ByteBuf 進(jìn)行后續(xù)的內(nèi)存泄露追蹤。
public final class UnpooledByteBufAllocator {
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
final ByteBuf buf;
....... 分配 UnpooledByteBuf .....
// 是否啟動(dòng)內(nèi)存泄露探測(cè),如果啟動(dòng)則額外用 LeakAwareByteBuf 進(jìn)行包裝返回
return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
}
}
public class PooledByteBufAllocator {
@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
....... 分配 PooledByteBuf .....
// 如果內(nèi)存泄露探測(cè)開(kāi)啟,則用 LeakAwareByteBuf 包裝 PooledByteBuf 返回
return toLeakAwareBuffer(buf);
}
}
Netty 為了實(shí)現(xiàn)對(duì) ByteBuf 內(nèi)存泄露的追蹤,從而引入了第四個(gè)模型 —— LeakAwareBuffer,從命名上就可以看出,LeakAwareBuffer 主要是為了識(shí)別出被其包裝的 ByteBuf 是否有內(nèi)存泄露情況的發(fā)生。
每當(dāng)命中采樣概率之后,Netty 都會(huì)將普通的 ByteBuf 包裝成一個(gè) LeakAwareBuffer 返回。
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
// DefaultResourceLeak 用于追蹤 ByteBuf 的泄露路徑
ResourceLeakTracker<ByteBuf> leak;
switch (ResourceLeakDetector.getLevel()) {
case SIMPLE:
// 觸發(fā)內(nèi)存泄露采樣探測(cè),如果命中采樣頻率
// 則為 ByteBuf 創(chuàng)建 DefaultResourceLeak(弱引用)
leak = AbstractByteBuf.leakDetector.track(buf); // 本文 3.1 小節(jié)內(nèi)容
if (leak != null) {
// SIMPLE 級(jí)別對(duì)應(yīng)的是 SimpleLeakAwareByteBuf
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
// 觸發(fā)內(nèi)存泄露采樣探測(cè)
leak = AbstractByteBuf.leakDetector.track(buf); // 本文 3.1 小節(jié)內(nèi)容
if (leak != null) {
// ADVANCED , PARANOID 級(jí)別對(duì)應(yīng)的是 AdvancedLeakAwareByteBuf
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
default:
break;
}
// 如果命中采樣頻率,則用 LeakAwareByteBuf 包裝返回
// 如果沒(méi)有命中采樣頻率,則原樣返回
return buf;
}
內(nèi)存泄露探測(cè)級(jí)別是 SIMPLE 的情況下,Netty 會(huì)用 SimpleLeakAwareByteBuf 對(duì) ByteBuf 進(jìn)行包裝。內(nèi)存泄露探測(cè)級(jí)別是 ADVANCED 或者 PARANOID 的情況下,Netty 會(huì)用 AdvancedLeakAwareByteBuf 對(duì) ByteBuf 進(jìn)行包裝。

從類的繼承結(jié)構(gòu)圖中我們可以看出,SimpleLeakAwareByteBuf 和 AdvancedLeakAwareByteBuf 均繼承于 WrappedByteBuf,說(shuō)明它們只是對(duì)原始普通 ByteBuf 的一個(gè)簡(jiǎn)單裝飾(裝飾者設(shè)計(jì)模型)。
class SimpleLeakAwareByteBuf extends WrappedByteBuf {
// 需要被探測(cè)的普通 ByteBuf
private final ByteBuf trackedByteBuf;
// ByteBuf 的弱引用 DefaultResourceLeak
final ResourceLeakTracker<ByteBuf> leak;
SimpleLeakAwareByteBuf(ByteBuf wrapped, ResourceLeakTracker<ByteBuf> leak) {
this(wrapped, wrapped, leak);
}
SimpleLeakAwareByteBuf(ByteBuf wrapped, ByteBuf trackedByteBuf, ResourceLeakTracker<ByteBuf> leak) {
super(wrapped);
this.trackedByteBuf = ObjectUtil.checkNotNull(trackedByteBuf, "trackedByteBuf");
this.leak = ObjectUtil.checkNotNull(leak, "leak");
}
}
LeakAwareByteBuf 中最核心的一個(gè)裝飾屬性就是 leak ,它用來(lái)指向與 trackedByteBuf 弱引用關(guān)聯(lián)的 DefaultResourceLeak。在 DefaultResourceLeak 剛被創(chuàng)建出來(lái)的時(shí)候,它會(huì)加入到全局的 allLeaks 集合中。

最開(kāi)始 DefaultResourceLeak 棧中只包含一個(gè) TraceRecord,位于棧底,用于記錄 trackedByteBuf 的創(chuàng)建位置堆棧。在 SIMPLE 探測(cè)級(jí)別下,內(nèi)存泄露日志中也只會(huì)出現(xiàn) trackedByteBuf 的創(chuàng)建位置堆棧。

所以 SimpleLeakAwareByteBuf 相關(guān)的 read , write 方法并沒(méi)有什么特別之處,都是對(duì) trackedByteBuf 的簡(jiǎn)單代理。
class SimpleLeakAwareByteBuf extends WrappedByteBuf {
@Override
public byte readByte() {
return trackedByteBuf.readByte();
}
@Override
public ByteBuf writeByte(int value) {
trackedByteBuf.writeByte(value);
return this;
}
}
值得聊一下的是 SimpleLeakAwareByteBuf 的 release() 方法,當(dāng)我們使用完 SimpleLeakAwareByteBuf , 就需要及時(shí)的手動(dòng)釋放。如果 SimpleLeakAwareByteBuf 的引用計(jì)數(shù)為 0 ,就需要額外關(guān)閉內(nèi)存泄露的探測(cè),因?yàn)橐呀?jīng)及時(shí)釋放了,就不會(huì)存在內(nèi)存泄露的情況。
@Override
public boolean release() {
// 引用計(jì)數(shù)為 0
if (super.release()) {
// 關(guān)閉內(nèi)存泄露的探測(cè)
closeLeak();
return true;
}
return false;
}
private void closeLeak() {
boolean closed = leak.close(trackedByteBuf);
}
關(guān)閉 trackedByteBuf 的內(nèi)存泄露檢測(cè)核心步驟是:
首先將 DefaultResourceLeak 從 allLeaks 集合中刪除,因?yàn)?allLeaks 中保存的全部都是未被釋放的 trackedByteBuf 對(duì)應(yīng)的 DefaultResourceLeak 。
斷開(kāi) DefaultResourceLeak 與 trackedByteBuf 的弱引用關(guān)聯(lián),這樣一來(lái),當(dāng) trackedByteBuf 被 GC 之后,JVM 將不會(huì)把 DefaultResourceLeak 放入到 _reference_pending_list 中,反而會(huì)將 DefaultResourceLeak 與 trackedByteBuf 一起回收。這樣一來(lái),refQueue 中自然也不會(huì)出現(xiàn)這個(gè) DefaultResourceLeak ,ResourceLeakDetector 也不會(huì)錯(cuò)誤地探測(cè)到它了。
public void clear() {
this.referent = null;
}
- 將 DefaultResourceLeak 棧中保存的 TraceRecords 清空。
private static final class DefaultResourceLeak<T>
extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
@Override
public boolean close() {
// 將 DefaultResourceLeak 從 allLeaks 集合中刪除
if (allLeaks.remove(this)) {
// 斷開(kāi) DefaultResourceLeak 與 trackedByteBuf 的弱引用關(guān)聯(lián)
clear();
// 清空 DefaultResourceLeak 棧
headUpdater.set(this, null);
return true;
}
return false;
}
如果這個(gè) SimpleLeakAwareByteBuf 忘記釋放了,那么它對(duì)應(yīng)的 DefaultResourceLeak 就會(huì)一直停留在 allLeaks 集合中,當(dāng) SimpleLeakAwareByteBuf 被 GC 之后,JVM 就會(huì)將 DefaultResourceLeak 放入到 _reference_pending_list 中,隨后喚醒 ReferenceHandler 線程將 DefaultResourceLeak 從 _reference_pending_list 中轉(zhuǎn)移到 refQueue。

當(dāng)下一次內(nèi)存分配的時(shí)候,如果命中內(nèi)存泄露采樣檢測(cè)的概率,那么 ResourceLeakDetector 就會(huì)從 refQueue 中將收集到的所有 DefaultResourceLeak 挨個(gè)摘下,并判斷它們是否仍然停留在 allLeaks 中。
如果仍然在 allLeaks 中,就說(shuō)明該 DefaultResourceLeak 對(duì)應(yīng)的 ByteBuf 發(fā)生了內(nèi)存泄露,而具體的泄露路徑就保存在 DefaultResourceLeak 棧中,最后將泄露路徑以 ERROR 的日志級(jí)別打印出來(lái)。
public class ResourceLeakDetector<T> {
private void reportLeak() {
// Detect and report previous leaks.
for (;;) {
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
// 檢查 ByteBuf 對(duì)應(yīng)的 DefaultResourceLeak 是否仍然停留在 allLeaks 集合中
if (!ref.dispose()) {
// 如果不存在,則說(shuō)明 ByteBuf 已經(jīng)被及時(shí)的釋放了,不存在內(nèi)存泄露
continue;
}
// 當(dāng)探測(cè)到 ByteBuf 發(fā)生內(nèi)存泄露之后,這里會(huì)獲取 ByteBuf 相關(guān)的訪問(wèn)堆棧
String records = ref.getReportAndClearRecords();
// 打印泄露的堆棧路徑
reportTracedLeak(resourceType, records);
}
}
}
以上就是內(nèi)存泄露探測(cè)級(jí)別 SIMPLE 的實(shí)現(xiàn)邏輯,而 ADVANCED , PARANOID 級(jí)別的特點(diǎn)在于它們會(huì)收集詳細(xì)的訪問(wèn)堆棧,所以 AdvancedLeakAwareByteBuf 是在 SimpleLeakAwareByteBuf 的基礎(chǔ)之上對(duì)相關(guān)的訪問(wèn)方法,比如 read , write 等方法進(jìn)行裝飾,裝飾什么呢 ?就是每對(duì) AdvancedLeakAwareByteBuf 進(jìn)行一次訪問(wèn),就向 DefaultResourceLeak 棧中添加一次最新的堆棧信息。
final class AdvancedLeakAwareByteBuf extends SimpleLeakAwareByteBuf {
AdvancedLeakAwareByteBuf(ByteBuf buf, ResourceLeakTracker<ByteBuf> leak) {
super(buf, leak);
}
@Override
public byte readByte() {
// 記錄當(dāng)前訪問(wèn)的堆棧信息
recordLeakNonRefCountingOperation(leak);
return super.readByte();
}
@Override
public ByteBuf writeByte(int value) {
// 記錄當(dāng)前訪問(wèn)的堆棧信息
recordLeakNonRefCountingOperation(leak);
return super.writeByte(value);
}
static void recordLeakNonRefCountingOperation(ResourceLeakTracker<ByteBuf> leak) {
if (!ACQUIRE_AND_RELEASE_ONLY) {
// 向 DefaultResourceLeak 添加新的堆棧
leak.record();
}
}
}
但一個(gè)現(xiàn)實(shí)的問(wèn)題是,ByteBuf 中有那么多的方法,如果對(duì) ByteBuf 每一個(gè)方法的訪問(wèn)都要記錄堆棧的話,那內(nèi)存消耗就太大了,況且 DefaultResourceLeak 棧中的 TraceRecords 個(gè)數(shù),是會(huì)受到 -Dio.netty.leakDetection.targetRecords 限制的,不能無(wú)限向棧中添加。
因此 Netty 又為我們提供了一個(gè)新的 JVM 參數(shù) -Dio.netty.leakDetection.acquireAndReleaseOnly ,默認(rèn)為 false , 表示默認(rèn)情況下,對(duì) ByteBuf 的每一個(gè)方法的訪問(wèn)都需要記錄堆棧。
private static final String PROP_ACQUIRE_AND_RELEASE_ONLY = "io.netty.leakDetection.acquireAndReleaseOnly";
ACQUIRE_AND_RELEASE_ONLY = SystemPropertyUtil.getBoolean(PROP_ACQUIRE_AND_RELEASE_ONLY, false);
設(shè)置為 true 表示,只對(duì)明確要求記錄堆棧的方法進(jìn)行記錄,比如 touch 相關(guān)方法,retain() 方法,還有 release() 方法。其他的方法均不記錄堆棧。
@Override
public ByteBuf touch() {
leak.record();
return this;
}
@Override
public ByteBuf touch(Object hint) {
leak.record(hint);
return this;
}
@Override
public ByteBuf retain() {
leak.record();
return super.retain();
}
@Override
public boolean release() {
leak.record();
return super.release();
}
由于在 SIMPLE 探測(cè)級(jí)別下只會(huì)記錄創(chuàng)建堆棧,不會(huì)記錄訪問(wèn)堆棧,所以 SimpleLeakAwareByteBuf 的相關(guān)訪問(wèn)方法均不會(huì)調(diào)用 leak.record()。
class SimpleLeakAwareByteBuf extends WrappedByteBuf {
@Override
public ByteBuf touch() {
return this;
}
@Override
public ByteBuf touch(Object hint) {
return this;
}
}
總結(jié)
要想觸發(fā) Netty 的內(nèi)存泄露探測(cè)機(jī)制需要同時(shí)滿足以下五個(gè)條件:
應(yīng)用必須開(kāi)啟內(nèi)存泄露探測(cè)功能。
必須要等到 ByteBuf 被 GC 之后,內(nèi)存泄露才能探測(cè)的到,如果 GC 一直沒(méi)有觸發(fā),那么即使是 ByteBuf 沒(méi)有任何強(qiáng)引用或者軟引用了,內(nèi)存泄露的探測(cè)也將無(wú)從談起。
當(dāng) GC 發(fā)生之后,必須是要等到下一次分配內(nèi)存的時(shí)候,才會(huì)觸發(fā)內(nèi)存泄露的探測(cè)。如果沒(méi)有內(nèi)存申請(qǐng)的行為發(fā)生,那么內(nèi)存泄露的探測(cè)也不會(huì)發(fā)生。
Netty 并不會(huì)探測(cè)每一個(gè) ByteBuf 的泄露情況,而是根據(jù)一定的采樣間隔,進(jìn)行采樣探測(cè)。所以要想觸發(fā)內(nèi)存泄露的探測(cè),還需要達(dá)到一定的采樣間隔。
應(yīng)用的日志級(jí)別必須開(kāi)啟 Error 級(jí)別,因?yàn)閮?nèi)存泄露的報(bào)告,Netty 是以 Error 級(jí)別的日志輸出出來(lái)的,如果日志級(jí)別在 Error 以下,那么內(nèi)存泄露的報(bào)告則無(wú)法輸出。
我們可以通過(guò) JVM 參數(shù) -Dio.netty.leakDetection.level為應(yīng)用設(shè)置不同的探測(cè)級(jí)別:
DISABLED 表示禁用內(nèi)存泄露探測(cè)。
SIMPLE 則是進(jìn)行內(nèi)存泄露的采樣探測(cè),我們可以通過(guò) JVM 參數(shù)
-Dio.netty.leakDetection.samplingInterval來(lái)設(shè)置內(nèi)存泄露探測(cè)的采樣頻率。內(nèi)存泄露報(bào)告中只會(huì)包含 ByteBuf 的創(chuàng)建位置堆棧信息。ADVANCED 也是進(jìn)行采樣探測(cè),但在內(nèi)存泄露報(bào)告中會(huì)體現(xiàn)更詳細(xì)的信息,比如,ByteBuf 的相關(guān)訪問(wèn)路徑堆棧信息,能夠采集到的泄露堆棧受到
-Dio.netty.leakDetection.targetRecords參數(shù)的限制。PARANOID 則是在 ADVANCED 的基礎(chǔ)之上,對(duì)系統(tǒng)中的所有 ByteBuf 進(jìn)行全量探測(cè)。級(jí)別最高,信息最全,消耗也最大。
好了,今天的內(nèi)容就到這里,我們下篇文章見(jiàn)~~~~~