Netty如何監控內存泄露

Netty如何監控內存泄露

[TOC]java

前言

通常而言,在Netty程序中都會採用池化的ByteBuf,也就是PooledByteBuf以提升程序性能。可是PooledByteBuf須要在使用完畢後手工釋放,不然就會由於PooledByteBuf申請的內存空間沒有歸還進而形成內存泄露,最終OOM。而一旦泄露發生,在複雜的應用程序中找到未手工釋放的ByteBuf並非一個簡單的活計,在沒有工具輔助的狀況只能白盒檢查全部源碼,效率無疑十分低下。dom

爲了解決這個問題,Netty設計了專門的泄露檢測接口用於實現對須要手動釋放的資源對象的監控。ide

JDK的弱引用和引用隊列

在分析Netty的泄露監控功能以前,先來複習下其中會用到的JDK知識:引用。工具

在java中存在4中引用類型,分別是強引用,軟引用,弱引用,虛引用。性能

強引用this

強引用,是咱們寫程序最常用的方式。好比一個將一個值賦給一個變量,那這個對象值就被該變量強引用了。除非設置爲null,不然java的內存回收不會回收該對象。就算是內存不足異常發生也不會。.net

軟引用設計

軟引用所引用的對象會在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的實現思路

針對須要手動關閉的資源對象,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. 關閉:這種模式下不進行泄露監控。
  2. 簡單:這種模式下以1/128的機率抽取ByteBuf進行泄露監控。
  3. 加強:在簡單的基礎上,每一次對ByteBuf的調用都會嘗試記錄調用軌跡,消耗較大。
  4. 偏執:在加強的基礎上,對每個ByteBuf都進行泄露監控,消耗最大。

通常而言,在項目的初期使用簡單模式進行監控,若是沒有問題一段時間後就能夠關閉。不然升級到加強或者偏執模式嘗試確認泄露位置。

追蹤和檢查泄露

泄露的檢查和追蹤主要依靠兩個類io.netty.util.ResourceLeakDetector.DefaultResourceLeakio.netty.util.ResourceLeakDetector.前者用於追蹤一個資源對象,而且記錄對應的調用軌跡;後者則負責管理和生成DefaultResourceLeak對象。

DefaultResourceLeak

首先來看用於追蹤資源對象的監控對象。該類繼承了WeakReference,有幾個重要的屬性,以下

//存儲着最新的調用軌跡信息,record內部經過next指針造成一個單向鏈表
private volatile Record head;
//調用軌跡不會無限制的存儲,有一個上限閥值。超過了閥值會拋棄掉一些調用軌跡信息。
private volatile int droppedRecords;
//存儲着全部的追蹤對象,用於確認追蹤對象是否處於可用。
private final Set<DefaultResourceLeak<?>> allLeaks;
//記錄追蹤對象的hash值,用於後續操做中的對象對比。
private final int trackedHash;

這個類的做用有三個:

  1. 調用record方法記錄調用軌跡
  2. 調用close方法結束追蹤
  3. 以及自己做爲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的思路總結下也很簡單,歸納以下:

  1. 使用CAS方式當前的調用軌跡對象Record設置爲head屬性的值。
  2. Record對象中的pos屬性記錄着當前軌跡鏈的長度,當追蹤對象的軌跡隊鏈的長度超過配置值時,有必定的概率(1-1/2<sup>min(n-target_record,30)</sup>)將最新的軌跡對象從鏈條中刪除。
  3. CAS成功後,若是有拋棄頭部的軌跡對象,則拋棄計數+1。

步驟2中在鏈條過長時選擇刪除最新的軌跡對象是基於如下兩點出發:

  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。不過該方法須要在下文的報告泄露中才會具有做用,這邊先暫留。

ResourceLeakDetector

該類用於按照規則進行追蹤對象的生成,外部主要是調用其方法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以前是否調用了DefaultResourceLeakclose方法。舉個例子,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,轉載請註明來源,謝謝。

歡迎掃碼關注

相關文章
相關標籤/搜索