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方法将传入的参数都保存到成员变量中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • 但是内存拷贝对性能有可能影响比较大,所以Java中可以绕开堆内存直接操作堆外内存,问题是创建堆外内存的速度比堆内存...
    zxRay阅读 7,372评论 2 32
  • 内存管理的主要目的合理分配内存,减少内存碎片,及时回收资源,提高内存的使用效率。从操作系统层面来说,各个软件在运行...
    史圣杰阅读 1,186评论 0 1
  • 进程 创建 创建进程用fork()函数。fork()为子进程创建新的地址空间并且拷贝页表。子进程的虚拟地址空间...
    梅花怒阅读 1,905评论 0 7
  • 抓主线,三个点: 虚拟内存组织 虚拟内存和物理内存的转换 物理内存组织 虚拟内存组织 平时在进程中,所谓的内存地址...
    123archu阅读 3,193评论 1 7
  • Netty作为一款高性能网络应用程序框架,实现了一套高性能内存管理机制 通过学习其中的实现原理、算法、并发设计,有...
    caison阅读 1,220评论 2 4