Netty源碼解析 -- PoolSubpage實現原理

前面文章說了PoolChunk如何管理Normal內存塊,本文分享PoolSubpage如何管理Small內存塊。
源碼分析基於Netty 4.1.52算法

內存管理算法

PoolSubpage負責管理Small內存塊。一個PoolSubpage中的內存塊size都相同,該size對應SizeClasses#sizeClasses表格的一個索引index。
新建立的PoolSubpage都必須加入到PoolArena#smallSubpagePools[index]鏈表中。
PoolArena#smallSubpagePools是一個PoolSubpage數組,數組中每一個元素都是一個PoolSubpage鏈表,PoolSubpage之間能夠經過next,prev組成鏈表。
感興趣的同窗能夠參考《內存對齊類SizeClasses》。數組

注意,Small內存size並不必定小於pageSize(默認爲8K)
默認Small內存size <= 28672(28KB)
關於Normal內存塊,Small內存塊,pageSize,可參考《PoolChunk實現原理》。微信

PoolSubpage實際上就是PoolChunk中的一個Normal內存塊,大小爲其管理的內存塊size與pageSize最小公倍數。
PoolSubpage使用位圖的方式管理內存塊。
PoolSubpage#bitmap是一個long數組,其中每一個long元素上每一個bit位均可以表明一個內存塊是否使用。源碼分析

內存分配

分配Small內存塊有兩個步驟this

  1. PoolChunk中分配PoolSubpage。

若是PoolArena#smallSubpagePools中已經有對應的PoolSubpage緩衝,則不須要該步驟。spa

  1. PoolSubpage上分配內存塊

PoolChunk#allocateSubpagecode

private long allocateSubpage(int sizeIdx) {
    // #1
    PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
    synchronized (head) {
        //allocate a new run
        // #2
        int runSize = calculateRunSize(sizeIdx);
        //runSize must be multiples of pageSize
        // #3
        long runHandle = allocateRun(runSize);
        if (runHandle < 0) {
            return -1;
        }
        // #4
        int runOffset = runOffset(runHandle);
        int elemSize = arena.sizeIdx2size(sizeIdx);

        PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
                           runSize(pageShifts, runHandle), elemSize);

        subpages[runOffset] = subpage;
        // #5
        return subpage.allocate();
    }
}

#1 這裏涉及修改PoolArena#smallSubpagePools中的PoolSubpage鏈表,須要同步操做
#2 計算內存塊size和pageSize最小公倍數
#3 分配一個Normal內存塊,做爲PoolSubpage的底層內存塊,大小爲Small內存塊size和pageSize最小公倍數
#4 構建PoolSubpage
runOffset,即Normal內存塊偏移量,也是該PoolSubpage在整個Chunk中的偏移量
elemSize,Small內存塊size
#5 在subpage上分配內存塊orm

PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int pageShifts, int runOffset, int runSize, int elemSize) {
    // #1
    this.chunk = chunk;
    this.pageShifts = pageShifts;
    this.runOffset = runOffset;
    this.runSize = runSize;
    this.elemSize = elemSize;
    bitmap = new long[runSize >>> 6 + LOG2_QUANTUM]; // runSize / 64 / QUANTUM
    init(head, elemSize);
}

void init(PoolSubpage<T> head, int elemSize) {
    doNotDestroy = true;
    if (elemSize != 0) {
        // #2
        maxNumElems = numAvail = runSize / elemSize;
        nextAvail = 0;
        bitmapLength = maxNumElems >>> 6;
        if ((maxNumElems & 63) != 0) {
            bitmapLength ++;
        }

        for (int i = 0; i < bitmapLength; i ++) {
            bitmap[i] = 0;
        }
    }
    // #3
    addToPool(head);
}

#1 bitmap長度爲runSize / 64 / QUANTUM,從《內存對齊類SizeClasses》能夠看到,runSize都是2^LOG2_QUANTUM的倍數。blog

#2
elemSize:每一個內存塊的大小
maxNumElems:內存塊數量
bitmapLength:bitmap使用的long元素個數,使用bitmap中一部分元素足以管理所有內存塊。
(maxNumElems & 63) != 0,表明maxNumElems不能整除64,因此bitmapLength要加1,用於管理餘下的內存塊。
#3 添加到PoolSubpage鏈表中索引

前面分析《Netty內存池與PoolArena》中說過,在PoolArena中分配Small內存塊時,首先會從PoolArena#smallSubpagePools中查找對應的PoolSubpage​。若是找到了,直接從該PoolSubpage​上分配內存。不然,分配一個Normal內存塊,建立PoolSubpage​,再在上面分配內存塊。

PoolSubpage#allocate

long allocate() {
    // #1
    if (numAvail == 0 || !doNotDestroy) {
        return -1;
    }
    // #2
    final int bitmapIdx = getNextAvail();
    // #3
    int q = bitmapIdx >>> 6;
    int r = bitmapIdx & 63;
    assert (bitmap[q] >>> r & 1) == 0;
    bitmap[q] |= 1L << r;
    // #4
    if (-- numAvail == 0) {
        removeFromPool();
    }
    // #5
    return toHandle(bitmapIdx);
}

#1 沒有可用內存塊,分配失敗。一般PoolSubpage分配完成後會從PoolArena#smallSubpagePools中移除,再也不在該PoolSubpage上分配內存,因此通常不會出現這種場景。
#2 獲取下一個可用內存塊的bit下標
#3 設置對應bit爲1,即已使用
bitmapIdx >>> 6,獲取該內存塊在bitmap數組中第q元素
bitmapIdx & 63,獲取該內存塊是bitmap數組中第q個元素的第r個bit位
bitmap[q] |= 1L << r,將bitmap數組中第q個元素的第r個bit位設置爲1,表示已經使用
#4 全部內存塊已分配了,則將其從PoolArena中移除。
#5 toHandle 轉換爲最終的handle

private int getNextAvail() {
    int nextAvail = this.nextAvail;
    if (nextAvail >= 0) {
        this.nextAvail = -1;
        return nextAvail;
    }
    return findNextAvail();
}

nextAvail爲初始值或free時釋放的值。
若是nextAvail存在,設置爲不可用後直接返回該值。
若是不存在,調用findNextAvail查找下一個可用內存塊。

private int findNextAvail() {
    final long[] bitmap = this.bitmap;
    final int bitmapLength = this.bitmapLength;
    // #1
    for (int i = 0; i < bitmapLength; i ++) {
        long bits = bitmap[i];
        if (~bits != 0) {
            return findNextAvail0(i, bits);
        }
    }
    return -1;
}

private int findNextAvail0(int i, long bits) {
    final int maxNumElems = this.maxNumElems;
    final int baseVal = i << 6;

    // #2
    for (int j = 0; j < 64; j ++) {
        if ((bits & 1) == 0) {
            int val = baseVal | j;
            if (val < maxNumElems) {
                return val;
            } else {
                break;
            }
        }
        bits >>>= 1;
    }
    return -1;
}

#1 遍歷bitmap,~bits != 0,表示存在一個bit位不爲1,即存在可用內存塊。
#2 遍歷64個bit位,
(bits & 1) == 0,檢查最低bit位是否爲0(可用),爲0則返回val。
val等於 (i << 6) | j,即i * 64 + j,該bit位在bitmap中是第幾個bit位。
bits >>>= 1,右移一位,處理下一個bit位。

內存釋放

釋放Small內存塊可能有兩個步驟

  1. 釋放PoolSubpage的上內存塊
  2. 若是PoolSubpage中的內存塊已所有釋放,則從Chunk中釋放該PoolSubpage,同時從PoolArena#smallSubpagePools移除它。

PoolSubpage#free

boolean free(PoolSubpage<T> head, int bitmapIdx) {
    if (elemSize == 0) {
        return true;
    }
    // #1
    int q = bitmapIdx >>> 6;
    int r = bitmapIdx & 63;
    assert (bitmap[q] >>> r & 1) != 0;
    bitmap[q] ^= 1L << r;

    setNextAvail(bitmapIdx);
    // #2
    if (numAvail ++ == 0) {
        addToPool(head);
        return true;
    }

    // #3
    if (numAvail != maxNumElems) {
        return true;
    } else {
        // #4
        if (prev == next) {
            // Do not remove if this subpage is the only one left in the pool.
            return true;
        }

        // #5
        doNotDestroy = false;
        removeFromPool();
        return false;
    }
}

#1 將對應bit位設置爲可使用
#2 在PoolSubpage的內存塊所有被使用時,釋放了某個內存塊,這時從新加入到PoolArena中。
#3 未徹底釋放,即還存在已分配內存塊,返回true
#4 邏輯到這裏,是處理全部內存塊已經徹底釋放的場景。
PoolArena#smallSubpagePools鏈表組成雙向鏈表,鏈表中只有head和當前PoolSubpage時,當前PoolSubpage的prev,next都指向head。
這時當前​PoolSubpage是PoolArena中該鏈表最後一個PoolSubpage,不釋放該PoolSubpage,以便下次申請內存時直接從該PoolSubpage上分配。
#5 從PoolArena中移除,並返回false,這時PoolChunk會將釋放對應Page節點。

void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
    if (isSubpage(handle)) {
        // #1
        int sizeIdx = arena.size2SizeIdx(normCapacity);
        PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);

        PoolSubpage<T> subpage = subpages[runOffset(handle)];
        assert subpage != null && subpage.doNotDestroy;

        synchronized (head) {
            // #2
            if (subpage.free(head, bitmapIdx(handle))) {
                //the subpage is still used, do not free it
                return;
            }
        }
    }

    // #3
    ...
}

#1
查找head節點,同步
#2
調用subpage#free釋放Small內存塊
若是subpage#free返回false,將繼續向下執行,這時會釋放PoolSubpage整個內存塊,不然,不釋放PoolSubpage內存塊。
#3 釋放Normal內存塊,就是釋放PoolSubpage整個內存塊。該部份內容可參考《PoolChunk實現原理》。

若是您以爲本文不錯,歡迎關注個人微信公衆號,系列文章持續更新中。您的關注是我堅持的動力!

相關文章
相關標籤/搜索