netty內存池之PoolArena

之前做了那麼多鋪墊,在這篇博文,我們將看清netty內存池管理的全貌。

PoolArena是一個抽象類,其子類爲HeapArena和DirectArena對應堆內存(heap buffer)和堆外直接內存(direct buffer),除了操作的內存(byte[]和ByteBuffer)不同外兩個類完全一致)。PoolArena管理了之前一系列的類,這裏講介紹它的實現細節。該類的實現接口是PoolArenaMetric,是一些信息的統計分析,我們暫時忽略。

下面來看下PoolArena的成員

    static final int numTinySubpagePools = 512 >>> 4;

    final PooledByteBufAllocator parent;

    private final int maxOrder;// chunk相關滿二叉樹的高度
    final int pageSize;// 單個page的大小
    final int pageShifts;// 用於輔助計算
    final int chunkSize;// chunk的大小
    final int subpageOverflowMask; // 用於判斷請求是否爲Small/Tiny
    final int numSmallSubpagePools;// small請求的雙向鏈表頭個數
    final int directMemoryCacheAlignment;// 對齊基準
    final int directMemoryCacheAlignmentMask;// 用於對齊內存
    private final PoolSubpage<T>[] tinySubpagePools;// Subpage雙向鏈表
    private final PoolSubpage<T>[] smallSubpagePools;// Subpage雙向鏈表

還有一些由PoolChunkList爲節點組成鏈表

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;

其中出現了tiny/small有必要解釋下,一圖勝千言,下面是我從網上找的圖片

即不同大小的內存塊,別叫不同的名稱,用於Chunk塊中的是Normal,正好8k爲1page,於是小於8k的內存塊成爲Tiny/Small,其中小於512B的爲Tiny。同理,Chunk塊存不下的內存塊爲Huge。

    enum SizeClass {
        Tiny,
        Small,
        Normal
        // 除此之外的請求爲Huge
    }

我們看下PoolArena的構造,構造挺多,我們一點一點分析。

 this.parent = parent;
        this.pageSize = pageSize;
        this.maxOrder = maxOrder;
        this.pageShifts = pageShifts;
        this.chunkSize = chunkSize;
        directMemoryCacheAlignment = cacheAlignment;
        directMemoryCacheAlignmentMask = cacheAlignment - 1;
        subpageOverflowMask = ~(pageSize - 1);
以上無非是成員賦初值。
        tinySubpagePools = newSubpagePoolArray(numTinySubpagePools);
        for (int i = 0; i < tinySubpagePools.length; i ++) {
            tinySubpagePools[i] = newSubpagePoolHead(pageSize);
        }

        numSmallSubpagePools = pageShifts - 9;
        smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools);
        for (int i = 0; i < smallSubpagePools.length; i ++) {
            smallSubpagePools[i] = newSubpagePoolHead(pageSize);
        }

對tiny/smallSubpagePool的初始化,跟我們之前分析的subpage關聯起來了。

q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
        q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
        q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
        q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
        q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
        qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);

        q100.prevList(q075);
        q075.prevList(q050);
        q050.prevList(q025);
        q025.prevList(q000);
        q000.prevList(null);
        qInit.prevList(qInit);

這幾個PoolChunkList的命名其實是有含義的。其實是按照內存的使用率來取名的,如qInit代表一個chunk最開始分配後會進入它,隨着其使用率增大會逐漸從q000到q100,而隨着內存釋放,使用率減小,它又會慢慢的從q100到q00。我們再來看下錶

狀態最小內存使用率最大內存使用率
QINIT125
Q0150
Q252575
Q5050100
Q7575100
Q100100100

也就是說,一條PoolChunkList對應上面相應的參數,其中的chunk使用率均符合其中的標準,否則會自動調整到相應的鏈中。

我們看下構造PoolChunkList鏈的方法。

    void prevList(PoolChunkList<T> prevList) {
        assert this.prevList == null;
        this.prevList = prevList;
    }

看上面的構造函數我們可以看出PoolChunkList爲節點的鏈的樣子


我們可以看到,如果chunk在Q25,當他使用率低於25則跑到Q0,再當他使用率爲0於是不再保留在內存中,其分配的內存被完全回收(它沒有前項指針)。再看看QInit,即使完全回收也不會被釋放,這樣始終保留在內存中(它前項指針指向自己),後面的分配就無需新建chunk,減小了分配的時間。

分配內存時先從內存佔用率相對較低的chunklist中開始查找,這樣查找的平均用時就會更短

    private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
            q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
            q075.allocate(buf, reqCapacity, normCapacity)) {
            return;
        }

        // Add a new chunk.
        PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
        long handle = c.allocate(normCapacity);
        assert handle > 0;
        c.initBuf(buf, handle, reqCapacity);
        qInit.add(c);
    }

源碼註釋告訴我們需要注意,上面這個方法已經是被synchronized修飾的了,因爲chunk本身的訪問不是線程安全的,因此我們在實際分配內存的時候必須保證線程安全,防止同一個內存塊被多個對象申請到。在這個方法中我們能看到,分配內存時的查找順序,先從低的開始找,但爲什麼不從q000開始?(網上找的答案,分析的非常到位!!)

在分析PoolChunkList的時候,我們知道一個chunk隨着內存的不停釋放,它本身會不停的往其所在的chunk list的prev list移動,直到其完全釋放後被回收。 如果這裏是從q000開始嘗試分配,雖然分配的速度可能更快了(因爲分配成功的機率更大),但一個chunk在使用率爲25%以內時有更大機率再分配,也就是一個chunk被回收的機率大大降低了。這樣就帶來了一個問題,我們的應用在實際運行過程中會存在一個訪問高峯期,這個時候內存的佔用量會是平時的幾倍,因此會多分配幾倍的chunk出來,而等高峯期過去以後,由於chunk被回收的機率降低,內存回收的進度就會很慢(因爲沒被完全釋放,所以無法回收),內存就存在很大的浪費。

爲什麼是從q050開始嘗試分配呢,q050是內存佔用50%~100%的chunk,猜測是希望能夠提高整個應用的內存使用率,因爲這樣大部分情況下會使用q050的內存,這樣在內存使用不是很多的情況下一些利用率低(<50%)的chunk慢慢就會淘汰出去,最終被回收。然而爲什麼不是從qinit中開始呢,這裏的chunk利用率低,但又不會被回收,豈不是浪費?q075,q100由於使用率高,分配成功的機率也會更小,因此放到最後(q100上的chunk使用率都是100%,爲什麼還要嘗試從這裏分配呢??)。

再往下,如果整個list都無法分配,創建一個新的chunk,加入到qinit中並分配空間。

我們再來順便看下huge的內存分配

    private void allocateHuge(PooledByteBuf<T> buf, int reqCapacity) {
        PoolChunk<T> chunk = newUnpooledChunk(reqCapacity);
        activeBytesHuge.add(chunk.chunkSize());
        buf.initUnpooled(chunk, reqCapacity);
        allocationsHuge.increment();
    }

直接使用了buf.initUnpooled(chunk, reqCapacity);沒用什麼優化策略,可能由於使用率不高。

是不是期待很久了,我們看下整內存分配的個過程吧

 private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
        final int normCapacity = normalizeCapacity(reqCapacity);
        if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
            int tableIdx;
            PoolSubpage<T>[] table;
            boolean tiny = isTiny(normCapacity);
            if (tiny) { // < 512
                if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                    // was able to allocate out of the cache so move on
                    return;
                }
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else {
                if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                    // was able to allocate out of the cache so move on
                    return;
                }
                tableIdx = smallIdx(normCapacity);
                table = smallSubpagePools;
            }

            final PoolSubpage<T> head = table[tableIdx];

            /**
             * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
             * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
             */
            synchronized (head) {
                final PoolSubpage<T> s = head.next;
                if (s != head) {
                    assert s.doNotDestroy && s.elemSize == normCapacity;
                    long handle = s.allocate();
                    assert handle >= 0;
                    s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
                    incTinySmallAllocation(tiny);
                    return;
                }
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
            }

            incTinySmallAllocation(tiny);
            return;
        }
        if (normCapacity <= chunkSize) {
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
                ++allocationsNormal;
            }
        } else {
            // Huge allocations are never served via the cache so just call allocateHuge
            allocateHuge(buf, reqCapacity);
        }
    }

終於把整個過程貼出來了,讓我們一點一點分析。先根據申請內存大小區分開來

1. tiny/small內存的話將table賦值爲tiny/smallsubPagePool。先從cache中獲取內存,失敗了則去對應的poolsubPage中去獲取(比如size爲16,則從tinylsubPagePool[0]中獲取,size爲512,則從smallsubPagePool[0]中獲取,以此類推),需要加鎖;如果雙向鏈表還沒初始化,則會使用Normal請求分配Chunk塊中的一個Page,Page以請求大小爲基準進行切分並分配第一塊內存,然後加入到雙向鏈表中(調用順序arena->chunkList->chunk->subpage)。

2.normal內存,先從cache中獲取,如果沒有則調用allocateNormal分配滿足要求的連續的Page塊。

3.對於Huge請求,則直接使用Unpooled直接分配。

其中內存大小類型有巧妙的位運算,可以看一下

    // capacity < pageSize
    boolean isTinyOrSmall(int normCapacity) {
        return (normCapacity & subpageOverflowMask) == 0;
    }

    // normCapacity < 512
    static boolean isTiny(int normCapacity) {
        return (normCapacity & 0xFFFFFE00) == 0;
    }

下面來看下內存釋放的整個過程

    void free(PoolChunk<T> chunk, long handle, int normCapacity, PoolThreadCache cache) {
        if (chunk.unpooled) {
            int size = chunk.chunkSize();
            destroyChunk(chunk);
            activeBytesHuge.add(-size);
            deallocationsHuge.increment();
        } else {
            SizeClass sizeClass = sizeClass(normCapacity);
            if (cache != null && cache.add(this, chunk, handle, normCapacity, sizeClass)) {
                // cached so not free it.
                return;
            }

            freeChunk(chunk, handle, sizeClass);
        }
    }

如果內存是Huge類型,則直接釋放(調用抽象方法子類具體實現),並統計相關信息。否則,找出類型,並且可以緩存的話就緩存,否則釋放(調用freeChunk)。

    void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass) {
        final boolean destroyChunk;
        synchronized (this) {
            switch (sizeClass) {
            case Normal:
                ++deallocationsNormal;
                break;
            case Small:
                ++deallocationsSmall;
                break;
            case Tiny:
                ++deallocationsTiny;
                break;
            default:
                throw new Error();
            }
            destroyChunk = !chunk.parent.free(chunk, handle);
        }
        if (destroyChunk) {
            // destroyChunk not need to be called while holding the synchronized lock.
            destroyChunk(chunk);
        }
    }

其中parent是poolChunkList,free則是先釋放handle空間,再從對應的qXXX不斷內存裝填->q000最後如果有多出的chunk則,調用抽象方法destroy(chunk);(具體子類來實現)

可以注意到本類重寫了Object的finalize()方法,該可能會在方法對象被在gc前調用

    @Override
    protected final void finalize() throws Throwable {
        try {
            super.finalize();
        } finally {
            destroyPoolSubPages(smallSubpagePools);
            destroyPoolSubPages(tinySubpagePools);
            destroyPoolChunkLists(qInit, q000, q025, q050, q075, q100);
        }
    }

    private static void destroyPoolSubPages(PoolSubpage<?>[] pages) {
        for (PoolSubpage<?> page : pages) {
            page.destroy();
        }
    }

    private void destroyPoolChunkLists(PoolChunkList<T>... chunkLists) {
        for (PoolChunkList<T> chunkList: chunkLists) {
            chunkList.destroy(this);
        }
    }

本類還一個值得一看的方法,重新分配內存

    void reallocate(PooledByteBuf<T> buf, int newCapacity, boolean freeOldMemory) {
        if (newCapacity < 0 || newCapacity > buf.maxCapacity()) {
            throw new IllegalArgumentException("newCapacity: " + newCapacity);
        }

        int oldCapacity = buf.length;
        if (oldCapacity == newCapacity) {
            return;
        }

        PoolChunk<T> oldChunk = buf.chunk;
        long oldHandle = buf.handle;
        T oldMemory = buf.memory;
        int oldOffset = buf.offset;
        int oldMaxLength = buf.maxLength;
        int readerIndex = buf.readerIndex();
        int writerIndex = buf.writerIndex();

        allocate(parent.threadCache(), buf, newCapacity);
        if (newCapacity > oldCapacity) {
            memoryCopy(
                    oldMemory, oldOffset,
                    buf.memory, buf.offset, oldCapacity);
        } else if (newCapacity < oldCapacity) {
            if (readerIndex < newCapacity) {
                if (writerIndex > newCapacity) {
                    writerIndex = newCapacity;
                }
                memoryCopy(
                        oldMemory, oldOffset + readerIndex,
                        buf.memory, buf.offset + readerIndex, writerIndex - readerIndex);
            } else {
                readerIndex = writerIndex = newCapacity;
            }
        }

        buf.setIndex(readerIndex, writerIndex);

        if (freeOldMemory) {
            free(oldChunk, oldHandle, oldMaxLength, buf.cache);
        }
    }

一點一點分析,如果重新分配內存跟原內存大小一致,那麼直接返回。先重新申請一段所需大小的空間,如果原來申請的內存大小小於新申請的,那麼把原內存的內容拷貝到新內存中,否則,把原內存可讀的部分數據拷貝過來,但如果連可讀的數據大小都比新申請的內存大小要大,那麼沒可讀的內存了。設置好readIndex跟writeIndex,然後把原內存釋放。

有沒有發現到這裏,整個內存池的過程基本上理清了,Arene->chunkList->chunk,Arena->subPage。

剩下cache裏面的細節跟往外PooledByteBuf的細節(有沒有發現這個已經接觸到了buf,快到使用層面了,read/writeIndex也是很眼熟的吧)是我們沒梳理的。

最後列舉下剩下的抽象方法:

    // 判斷子類實現Heap還是Direct
    abstract boolean isDirect();
    // 新建一個Chunk,Tiny/Small,Normal請求請求分配時調用
    protected abstract PoolChunk<T> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize);
    // 新建一個Chunk,Huge請求分配時調用
    protected abstract PoolChunk<T> newUnpooledChunk(int capacity);
    protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity);
    // 複製內存,當ByteBuf擴充容量時調用
    protected abstract void memoryCopy(T src, int srcOffset, T dst, int dstOffset, int length);
    // 銷燬Chunk,釋放內存時調用
    protected abstract void destroyChunk(PoolChunk<T> chunk);



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章