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

binecy發表於2020-12-19

前面文章說了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記憶體塊有兩個步驟

  1. PoolChunk中分配PoolSubpage。
    如果PoolArena#smallSubpagePools中已經有對應的PoolSubpage緩衝,則不需要該步驟。
  2. PoolSubpage上分配記憶體塊

PoolChunk#allocateSubpage

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上分配記憶體塊

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的倍數。

#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實現原理》。

如果您覺得本文不錯,歡迎關注我的微信公眾號,系列文章持續更新中。您的關注是我堅持的動力!

相關文章