1. 程式人生 > >Netty4原始碼分析之記憶體管理

Netty4原始碼分析之記憶體管理

引用計數
netty中使用引用計數機制來管理資源,當一個實現ReferenceCounted的物件例項化時,引用計數置1.
客戶程式碼中需要保持一個該物件的引用時需要呼叫介面的retain方法將計數增1.物件使用完畢時呼叫release將計數減1.
當引用計數變為0時,物件將釋放所持有的底層資源或將資源返回資源池.

記憶體洩露
按上述規則使用Direct和Pooled的ByteBuf尤其重要.對於DirectBuf,其記憶體不受VM垃圾回收控制只有在呼叫release導致計數為0時才會主動釋放記憶體,而PooledByteBuf只有在release後才能被回收到池中以迴圈利用.
如果客戶程式碼沒有按引用計數規則使用這兩種物件,將會導致記憶體洩露.


記憶體使用跟蹤
在netty.io.util包中含有如下兩個類
ResourceLeak 用於跟蹤記憶體洩露
ResourceLeakDetector 記憶體洩露檢測工具

在io.netty.buffer.AbstractByteBufAllocator類中有如下程式碼

//裝飾器模式,用SimpleLeakAwareByteBuf或AdvancedLeakAwareByteBuf來包裝原始的ByteBuf
//兩個包裝類均通過呼叫ResourceLeak的record方法來記錄ByteBuf的方法呼叫堆疊,區別在於後者比前者記錄更多的內容
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
    ResourceLeak leak;
    //根據設定的Level來選擇使用何種包裝器
    switch (ResourceLeakDetector.getLevel()) {
        case SIMPLE:
            //建立用於跟蹤和表示內容洩露的ResourcLeak物件
            leak = AbstractByteBuf.leakDetector.open(buf);
            if (leak != null) {
                //只在ByteBuf.order方法中呼叫ResourceLeak.record
                buf = new SimpleLeakAwareByteBuf(buf, leak);
            }
            break;
        case ADVANCED:
        case PARANOID:
            leak = AbstractByteBuf.leakDetector.open(buf);
            if (leak != null) {
                //在ByteBuf幾乎所有方法中呼叫ResourceLeak.record  
                buf = new AdvancedLeakAwareByteBuf(buf, leak);
            }
            break;
    }
    return buf;
}

下圖展示了該方法被呼叫的時機.可見Netty只對PooledByteBuf和DirectByteBuf監控記憶體洩露.



記憶體洩露檢測

下面觀察上述程式碼中的AbstractByteBuf.leakDetector.open(buf);

實現程式碼如下
//建立用於跟蹤和表示內容洩露的ResourcLeak物件
public ResourceLeak open(T obj) {
    Level level = ResourceLeakDetector.level;
    if (level == Level.DISABLED) {//禁用記憶體跟蹤
        return null;
    }
    if (level.ordinal() < Level.PARANOID.ordinal()) {
        //如果監控級別低於PARANOID,在一定的取樣頻率下報告記憶體洩露
        if (leakCheckCnt ++ % samplingInterval == 0) {
            reportLeak(level);
            return new DefaultResourceLeak(obj);
        } else {
            return null;
        }
    } else {
        //每次需要分配 ByteBuf 時,報告記憶體洩露情況
        reportLeak(level);
        return new DefaultResourceLeak(obj);
    }
}

其中reportLeak方法中完成對記憶體洩露的檢測和報告,如下面程式碼所示.

private void reportLeak(Level level) {
    //......

    // 報告生成了太多的活躍資源
    int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
    if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
        logger.error("LEAK: You are creating too many " + resourceType + " instances.  " +
                resourceType + " is a shared resource that must be reused across the JVM," +
                "so that only a few instances are created.");
    }

    // 檢測並報告之前發生的記憶體洩露
    for (;;) {
        @SuppressWarnings("unchecked")
        //檢查引用佇列(為什麼通過檢查該佇列,可以判斷是否存在記憶體洩露)
        DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
        if (ref == null) {//佇列為空,沒有未報告的記憶體洩露或者從未發生記憶體洩露
            break;
        }
<span style="white-space:pre">	</span>//清理引用
        ref.clear();

        if (!ref.close()) {
            continue;
        }
        //通過錯誤日誌列印資源的方法呼叫記錄,並將其儲存在reportedLeaks中
        String records = ref.toString();
        if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
            if (records.isEmpty()) {
                logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
                        "Enable advanced leak reporting to find out where the leak occurred. " +
                        "To enable advanced leak reporting, " +
                        "specify the JVM option '-D{}={}' or call {}.setLevel()",
                        resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
            } else {
                logger.error(
                        "LEAK: {}.release() was not called before it's garbage-collected.{}",
                        resourceType, records);
            }
        }
    }
}
綜合上面的三段程式碼,可以看出, Netty 在分配新 ByteBuf 時進行記憶體洩露檢測和報告.

DefaultResourceLeak的宣告如下

private final class DefaultResourceLeak extends PhantomReference<Object> implements ResourceLeak{
    //......

    public DefaultResourceLeak(Object referent) {
        //使用一個靜態的引用佇列(refQueue)初始化
        //refQueue是ResourceLeakDecetor的成員變數並由其初始化
        super(referent, referent != null? refQueue : null);
        //......
    }

    //......
}

可見DefaultResourceLeak是個”虛”引用型別,有別於常見的普通的”強”引用,虛引用完全不影響目標物件的垃圾回收,但是會在目標物件被VM垃圾回收時被加入到引用佇列中.
在正常情況下ResourceLeak物件會所監控的資源的引用計數為0時被清理掉(不在被加入引用佇列),所以一旦資源的引用計數失常,ResourceLeak物件會被加入到引用佇列.例如沒有成對呼叫ByteBuf的retain和relaease方法,導致ByteBuf沒有被正常釋放(對於DirectByteBuf沒有及時釋放記憶體,對於PooledByteBuf沒有返回Pool),當引用佇列中存在元素時意味著程式中有記憶體洩露發生.
ResourceLeakDetector通過檢查引用佇列來判斷是否有記憶體洩露,並報告跟蹤情況.

總結
Netty使用裝飾器模式,為ByteBuf增加記憶體跟蹤記錄功能.利用虛引用跟蹤資源被VM垃圾回收的情況,加上ByteBuf的引用計數特性,進而判斷是否發生記憶體洩露.