9.PoolSubpage

PoolSubPage

在PoolChunk分配内存时,如果normCapacity & subpageOverflowMask = 0,也就是申请分配的内存大小不到一个Page,则会申请分配SubPage。事实上早在PoolChunk构造函数中就初始化了一个成员变量subpages,这是一个PoolSubPage的数组,长度为2^maxOrder,默认2048,与Page二叉树叶子结点数量一致。此外在PoolArena里也有2个PoolSubpage数组,它们被命名为tinySubpagePools和smallSubpagePools。
tinySubpage是指subpage大小在16B496B之间的数组,以16B的数量递增,数组长度为512B/16B=32;smallSubpage是指subpage大小在512B4MB之间的数组,以2的指数递增,表示分别分配512KB,1MB,2MB,4MB大小的smallSubpage,因此它的长度为4。

铺垫完之后,先看一下PoolSubPage初始化过程。

PoolSubPage初始化

PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
    this.chunk = chunk;
    this.memoryMapIdx = memoryMapIdx;
    this.runOffset = runOffset;
    this.pageSize = pageSize;
    bitmap = new long[pageSize >>> 10];
    init(head, elemSize);
}
void init(PoolSubpage<T> head, int elemSize) {
    doNotDestroy = true;
    this.elemSize = elemSize;
    if (elemSize != 0) {
        maxNumElems = numAvail = pageSize / elemSize;
        nextAvail = 0;
        bitmapLength = maxNumElems >>> 6;
        if ((maxNumElems & 63) != 0) {
            bitmapLength ++;
        }
        for (int i = 0; i < bitmapLength; i ++) {
            bitmap[i] = 0;
        }
    }
    addToPool(head);
}
private void addToPool(PoolSubpage<T> head) {
    prev = head;
    next = head.next;
    next.prev = this;
    head.next = this;
}

显然,构造函数可以根据调用的方法数分成3个步骤

  1. 初始化chunk等成员变量。
  2. 计算最大poolSubpage数量。
  3. 添加poolSubpage到PoolArena的双向链表中。
    一个个步骤来讲述

初始化chunk等成员变量

先介绍一下这里面涉及到的成员变量。head是一个特殊的PoolSubpage,位于PoolArena中;chunk即是分配所在的PoolChunk;memoryMap是Chunk中的满二叉树;runOffset是此次分配相对于满二叉树数组的偏移量;pageSize是Chunk中的pageSize,默认8KB;elemSize是一个规整化后的结果,在初次分配时,当前page将按这个大小进行切分,之后相同大小的PoolSubpage将组成双向链表。bitmap是一个long类型数组,netty使用它来描述PoolSubpage双向链表中的内存分配情况,数组长度为pageSize >>> 10,默认为8,这里需要解释一下这个长度:由于一个Page默认大小为8KB,而PoolSubpage大小最小为16B,因此最多可以分配8192 / 16 = 512个poolSubpage。由于long类型数字有64位,所以最多需要8个long元素组成的数组来描述分配情况。

计算最大poolSubpage数量

maxNumElems表示最多可能拥有的poolSubpage数量,numAvial表示当前还可分配的poolSubpage数量。在未使用时,maxNumElems和numAvial相等,都为pageSize / elemSize,相关逻辑可以参考上文biamap的描述。
bitmapLength表示bitmap数组的长度,因为PoolSubpage并不都只有16B,大一些的PoolSubpage不需要完整的8个long来表示。这个值通过将PoolSubpage除以代表long类型长度的64得到。如果maxNumElems & 63 != 0,表明maxNumElems不是64的整数倍,需要为这个余数单独增加一个bitmap元素。之后将所有bitmap元素都初始化为0.

添加poolSubpage到PoolArena的双向链表中

PoolArena的2个PoolSubpage数组默认都会被设置为head,head并不承载数据,只作为一个哨兵节点存在。这里添加到PoolArena就是一个简单的双向链表操作,只需要注意每次插入PoolSubpage都是插入在head之后,也即头插法。在这之后PoolArena分配相同大小的内存时,可以直接进行分配,而不必先初始化PoolChunk,分配Page后再分配PoolSubpage。

PoolSubpage分配内存

在介绍完PoolSubpage的初始化和一些成员变量后,开始分析它的内存分配过程。

private long allocateSubpage(int normCapacity) {
    PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
    int d = maxOrder;
    synchronized (head) {
        int id = allocateNode(d);
        if (id < 0) {
            return id;
        }
        final PoolSubpage<T>[] subpages = this.subpages;
        final int pageSize = this.pageSize;
        freeBytes -= pageSize;
        int subpageIdx = subpageIdx(id);
        PoolSubpage<T> subpage = subpages[subpageIdx];
        if (subpage == null) {
            subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
        } else {
            subpage.init(head, normCapacity);
        }
        return subpage.allocate();
    }
}

分配过程可以分为5个步骤

  1. 根据分配的大小找到PoolArena中对应的PoolSubpage数组
  2. 找到可分配的Page节点,并在可用空间中减去Page节点的容量。这一步骤与PoolChunk寻找Page节点完全一样,不再赘述。
  3. 找到在PoolChunk中subpage对应的索引值。
  4. 初始化subpage。
  5. 分配subpage级别的内存。

找到对应PoolSubpage数组

前文说过,PoolArena中有2个PoolSubpage数组,数组中的每一个元素实际上都会形成一个双向链表,而链表头结点就是head。这里倒是有那么一点hashmap的样子,但hashmap是通过对key取hash值再对数组长度取模来定位到数组下标,而PoolSubpage则是根据将要分配的大小。

PoolSubpage<T> findSubpagePoolHead(int elemSize) {
    int tableIdx;
    PoolSubpage<T>[] table;
    if (isTiny(elemSize)) { // < 512
        tableIdx = elemSize >>> 4;
        table = tinySubpagePools;
    } else {
        tableIdx = 0;
        elemSize >>>= 10;
        while (elemSize != 0) {
            elemSize >>>= 1;
            tableIdx ++;
        }
        table = smallSubpagePools;
    }

    return table[tableIdx];
}

当elemSize小于512时,PoolArena将会去tinyPoolSubpage数组查找,由于每个PoolSubpage以16B的容量递增,所以这里将elemSize右移4即除以16来计算数组下标。同理当elemSize大于等于512时,先右移10,若elemSize等于0,则说明elemSize等于512,定位到0号元素,否则逐步右移,意味着elemSize实际容量比预期要翻倍。定位到数组元素后,返回head节点。
在步骤2开始前,对head节点加了互斥锁,

找到subpage对应的索引值

这里就是利用上面查找到的Page节点在Page二叉树中的下标值menoryMapIdx与maxSubpageAllocs做异或运算,maxSubpageAllocs在数值上与Page二叉树叶子节点数相同。由于maxSubpageAllocs是2的幂次方,默认2048,转化为二进制是1000,0000,0000。而memoryMapIdx也是从2048开始到4095,转化为二进制是从1000,0000,0000到1111,1111,1111,两者做异或运算时由于首位都为1,则结果范围恰好一一在0-2047内,没有冲突。精妙的位运算运用。

private int subpageIdx(int memoryMapIdx) {
    return memoryMapIdx ^ maxSubpageAllocs;
}

初始化subpage

在构造函数中有一个参数runOffset,在前文带过了,这里再深入一下它是如何计算的。

private int runOffset(int id) {
    int shift = id ^ 1 << depth(id)
    return shift * runLength(id);
}

private int runLength(int id) {
    return 1 << log2ChunkSize - depth(id);
}

depth(id)就是从PoolChunk中那个不变深度的满二叉树depthMap中取得对应下标的深度。这里还需要注意顺序,是先计算右移再异或,因此这里也是计算这个节点相对于完全二叉树每一层最左边的偏移量。而runLength(id)则是计算id这一层的每单位偏移量为多少。两者相乘,就是当前相当于id所在层最左侧节点需要偏移多少量。当然由于这个方法只在PoolSubpage分配内存时调用,也可以当做相对于2048叶子节点的偏移量。其余初始化流程不赘述。

分配subpage级别的内存

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

老规矩,忽略检验性代码,分解成4个步骤

  1. 找出PoolSubpage中下一个可以分配的内存块。
  2. 将内存块的bitmapIdx标识为已分配。
  3. 如果该PoolSubpage不可分配了,从PoolArena中移除。
  4. 将bitmapIdx放到返回值的高32位中。

先看如何找到内存地址代表的bitmapIdx

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

在初始化时,nextAvial是为0的,说明还未申请分配。当第一次分配完后,nextAvial变成了-1,说明已被分配,或许这个变量命名为thisAvial会更好理解?而如果已被分配,则进入findNextAvial方法寻找下一个可用点内存块。

findNextAvial则是根据bitmap来寻找可用内存块。"~bits != 0"表示如果该内存块按位翻转后不为0,说明原本存在为0即未分配的节点,则进入该内存块寻找指定位置。

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

这里只要谨记参数i是bitmap数组中第i个元素,bits表示该元素的64位二进制。因此baseVal含义是把bitmap数组第i号元素左移6位。循环的目的是将bits不断右移,找到第一个不为0的二进制位。当找到时,将baseVal与j做或运算。这里也可以看出为什么baseVal左移6位,因为j最多只到63,刚好占用6个二进制位,两者不会有所冲突。

private int findNextAvail0(int i, long bits) {
    final int maxNumElems = this.maxNumElems;
    final int baseVal = i << 6;
    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;
}

找到bitmapIdx后,进入步骤2:通过将bitmapIdx右移6位,得到原本的i,与63做&运算,得到j,将bitmap[i]原本的值与j做或运算,表示将bitmap数组第i号元素的第j位内存块标识为已完成分配。

int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
bitmap[q] |= 1L << r;

分配完一个内存块后,将numAvial自减并顺便做了一个判断,numAvial为0意味着这个PoolSubpage不能再分配内存块了,所以将其从PoolArena中去掉。这里也是一个简单的双链表操作,不多描述。

private void removeFromPool() {
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
}

最后则是将bitmapIdx放到返回值的高32位中。

private long toHandle(int bitmapIdx) {
    return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}

这里可以结合PoolChunk的allocateRun方法的返回值一起思考:netty需要将2个返回值handle统一,所以将handle用long类型表示,其中long类型的低32位表示PoolSubpage所属的Page的下标,高32位中的7-10位表示bitmap数组中的元素号,低6位表示bitmap元素即一个long类型的实际分配情况。

初始化ByteBuf

在PoolChunk的allocate方法中,返回了handle后最终会进行Bytebuf的初始化,相关方法如下。

void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
    int memoryMapIdx = memoryMapIdx(handle);
    int bitmapIdx = bitmapIdx(handle);
    if (bitmapIdx == 0) {
        byte val = value(memoryMapIdx);
        buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
                reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
    } else {
        initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
    }
}

memoryMapIdx(handle)和bitmapIdx(handle)分别取handle的后32位和前32位,结合前文说的handle的含义,不难理解这2个方法。
然后进入了bitmapIdx==0的判断,如果条件成立,说明需要分配Page级别的Bytebuf,否则分配Subpage级别的Bytebuf。
两种分支都是根据handle计算offset,最终调用buf.init方法将传入的参数都保存到成员变量中。

posted @ 2020-01-31 19:28  朵巴阁  阅读(252)  评论(0编辑  收藏  举报