泄露監(jiān)測

背景

Netty中的ByteBuf是make things right的關鍵,對象本身可以被對象池回收,而它所占據(jù)的內存空間也可以被回收再分配,而這一切都是通過調用release來達成。

自從Netty 4開始,對象的生命周期由它們的引用計數(shù)管理,而不是由垃圾收集器管理了。Netty的原意是當引用計數(shù)歸零才需要去release, 由于JVM并沒有意識到Netty實現(xiàn)的引用計數(shù)對象,它仍會將這些引用計數(shù)對象當做常規(guī)對象處理,也就意味著,當不為0的引用計數(shù)對象變得不可達時仍然會被GC自動回收。一旦被GC回收,那么意味著該死的release我永遠都無法觸達,這樣便會造成內存泄露。舉個實際的經常犯的毛病, ByteBuf用完忘記release. 如果沒有一定的機制, 你可能永遠都發(fā)現(xiàn)不了.

當然, Netty的方案并沒有給社區(qū)提供包山包海通天的解決方案, 他是根據(jù)設定的頻率來檢測可能的泄漏, 最終通過日志告知開發(fā)者有泄露,要求開發(fā)者來排查問題。

引用

在深入Netty的解決方案前, 我們有必要先回顧下Java的幾種引用類型.

  1. 強引用,最普遍的引用,類似Object obj = new Object()這類的引用。只要強引用還存在,垃圾回收器就不會回收掉被引用的對象。當內存空間不足,JVM寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。
  2. 軟引用(SoftReference類),如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它,而如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現(xiàn)內存敏感的緩存。
  3. 弱引用(WeakReference類),弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。垃圾回收器進行對象掃描時,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。
  4. 虛/幻影引用(PhantomReference類),虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。而且也無法通過虛引用來取得一個對象實例。當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯(lián)的引用隊列ReferenceQueue中。

假如

當我們的資源被GC時, Phantom Reference隊列能取到指向它的 PhantomReference. 前提是這個PhantomReference不能是孤立的, 不然會被GC掉. 解決辦法也很簡單粗暴, 我們需要提供一個容器來托管他們, 只要容器不倒, 他們就不會消失. 一旦該資源被成功release, 那么立即從這個容器中移除掉. 那么該資源的PhantomReference不久就會被GC掉.

泄露監(jiān)測

protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
    PoolThreadCache cache = threadCache.get();
    PoolArena<byte[]> heapArena = cache.heapArena;

    ByteBuf buf;
    if (heapArena != null) {
        buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
    } else {
        buf = new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
    }
    // 在新建ByteBuf的時候, 會開始監(jiān)控該buf是否會泄漏
    return toLeakAwareBuffer(buf);
}
// 裝飾器模式, 對現(xiàn)有buf的增強
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
    ResourceLeak leak;
    switch (ResourceLeakDetector.getLevel()) {
        // 至于下面的level不是重點, 內存泄漏的監(jiān)控也是要成本的, 就看怎么取舍
        // 而不同的level都會去到AbstractByteBuf.leakDetector.open
        // 這里很形象,就是告訴leakDetector我要檢測這個對象, 如果發(fā)生泄漏上報給我.
        case SIMPLE:
            leak = AbstractByteBuf.leakDetector.open(buf);
            if (leak != null) {
                buf = new SimpleLeakAwareByteBuf(buf, leak);
            }
            break;
        case ADVANCED:
        case PARANOID:
            leak = AbstractByteBuf.leakDetector.open(buf);
            if (leak != null) {
                buf = new AdvancedLeakAwareByteBuf(buf, leak);
            }
            break;
        default:
            break;
    }
    return buf;
}

public final ResourceLeak open(T obj) {
    Level level = ResourceLeakDetector.level;
    if (level == Level.DISABLED) {
        return null;
    }

    if (level.ordinal() < Level.PARANOID.ordinal()) {
        // 每隔128次泄漏檢查就要出具報告一次
        if ((++ leakCheckCnt & mask) == 0) {
            reportLeak(level);
            return new DefaultResourceLeak(obj);
        } else {
            return null;
        }
    } else {
        reportLeak(level);
        return new DefaultResourceLeak(obj);
    }
}
private void reportLeak(Level level) {
    // 首先你的日志級別要是error, 否則將refQueue里面對象全部清掉
    // 換句話說, 只要日志不對, 泄漏檢測就什么都不做
    if (!logger.isErrorEnabled()) {
        for (;;) {
            @SuppressWarnings("unchecked")
            DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
            if (ref == null) {
                break;
            }
            ref.close();
        }
        return;
    }

    // 如果你申請監(jiān)控的資源對象太多也要提醒開發(fā)者.
    int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
    if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
        reportInstancesLeak(resourceType);
    }

    // 遍歷refQueue
    for (;;) {
        @SuppressWarnings("unchecked")
        DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
        if (ref == null) {
            break;
        }
        
        // 這里保證ref不會再回到refQueue里面
        ref.clear();

        // 這里是將DefaultResourceLeak從隊列中刪除, 也就是從觀察名單中移除
        if (!ref.close()) {
            continue;
        }
        
        // 接下來就是生成這個資源對象的泄露報告了
        // 這里的records主要是該資源在每次retain的時候,視情況去記錄軌跡,說白了就是使用記錄
        // 如果返回空,那么只上報基本情況,否則將軌跡一起上報.
        // 里面很簡單, 就不再深入了
        String records = ref.toString();
        if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
            if (records.isEmpty()) {
                reportUntracedLeak(resourceType);
            } else {
                reportTracedLeak(resourceType, records);
            }
        }
    }
}

容器

關鍵屬性

// 創(chuàng)建記錄
private final String creationRecord;
// 引用記錄軌跡
private final Deque<String> lastRecords = new ArrayDeque<String>();
// 是否被close
private final AtomicBoolean freed;
// 前驅
private DefaultResourceLeak prev;
// 后繼
private DefaultResourceLeak next;
// 從上面就可以看出來這個容器是以DefaultResourceLeak為節(jié)點類型的雙向鏈表

// 刪除的軌跡記錄
private int removedRecords;
DefaultResourceLeak(Object referent) {
    // 包裝PhantomReference, 捎上refQueue, JVM垃圾回收時會將滿足條件的填入queue中
    super(referent, referent != null? refQueue : null);

    if (referent != null) {
        Level level = getLevel();
        if (level.ordinal() >= Level.ADVANCED.ordinal()) {
            creationRecord = newRecord(null, 3);
        } else {
            creationRecord = null;
        }

        // 將該兼容資源假如到這個雙向列表中.
        synchronized (head) {
            prev = head;
            next = head.next;
            head.next.prev = this;
            head.next = this;
            active ++;
        }
        freed = new AtomicBoolean();
    } else {
        creationRecord = null;
        freed = new AtomicBoolean(true);
    }
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容