Netty源码分析-PoolChunk

 

 

 

 

上图中是一个默认大小的chunk, 由2048个page组成了一个chunk,一个page的大小为8192, chunk之上有11层节点,最后一层节点数与page数量相等。每次内存分配需要保证内存的连续性,这样才能简单的操作分配到的内存,因此这里构造了一颗完整的平衡二叉树,所有子节点的管理的内存也属于其父节点。如果我们想获取一个8K的内存,则只需在第11层找一个可用节点即可,而如果我们需要16K的数据,则需要在第10层找一个可用节点。如果一个节点存在一个已经被分配的子节点,则该节点不能被分配,例如我们需要16K内存,这个时候id为2048的节点已经被分配,id为2049的节点未分配,就不能直接分配1024这个节点,因为这个节点下的内存只有8K了。

通过上面这个树结构,我们可以看到每次内存分配都是8K*(2^n), 比如需要24K内存时,实际上会申请到一块32K的内存。为了分配一个大小为chunkSize/(2^k)的内存段,需要在深度为k的层从左开始查找可用节点。如想分配16K的内存,chunkSize = 16M, 则k=10, 需要从第10层找一个空闲的节点分配内存。

如何高效的从这么多page中分配到指定的内存呢。来看看下面这个图:

 

 

这个图与上图结构一致,不同的是上方的二叉树的值为当前的层数,两张图和起来用一个数组memoryMap表示,上面的图中的数字表示数组的index,下面的图中的数字表示当前节点及其子节点可以分配的层的高度。如对于id=512的节点,其深度为9,则:

1)memoryMap[512] = 9,则表示其本身到下面所有的子节点都可以被分配;

2)memoryMap[512] = val (从10到11), 则表示512节点下有子节点已经分配过,则该节点不能直接被分配,而其子节点中的第val和val以下层还存在未分配的节点;

3)memoryMap[512] = 12 (即总层数 + 1), 可分配的深度已经大于总层数, 则该节点下的所有子节点都已经被分配。

下面我们在从源码分析下PoolChunk是如何实现的,首先看看它的构造方法:

 

PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize) {  
    unpooled = false;  
    this.arena = arena;  
    // memory是一个容量为chunkSize的byte[](heap方式)或ByteBuffer(direct方式)  
    this.memory = memory;  
    // 每个page的大小,默认为8192  
    this.pageSize = pageSize;  
        // 13,   2 ^ 13 = 8192  
   this.pageShifts = pageShifts;  
    // 默认11  
    this.maxOrder = maxOrder;  
    // 默认 8192 << 11 = 16MB  
    this.chunkSize = chunkSize;  
    // 12, 当memoryMap[id] = unusable时,则表示id节点已被分配  
    unusable = (byte) (maxOrder + 1);  
    // 24, 2 ^ 24 = 16M  
    log2ChunkSize = log2(chunkSize);  
    // -8192  
    subpageOverflowMask = ~(pageSize - 1);  
    freeBytes = chunkSize;  
  
    assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;  
    // 2048, 最多能被分配的Subpage个数  
    maxSubpageAllocs = 1 << maxOrder;  
  
    // Generate the memory map.  
    memoryMap = new byte[maxSubpageAllocs << 1];  
    depthMap = new byte[memoryMap.length];  
    int memoryMapIndex = 1;  
    // 分配完成后,memoryMap->[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3…]  
    // depthMap->[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3…]  
    for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time  
      int depth = 1 << d;  
      for (int p = 0; p < depth; ++ p) {  
        // in each level traverse left to right and set value to the depth of subtree  
        memoryMap[memoryMapIndex] = (byte) d;  
        depthMap[memoryMapIndex] = (byte) d;  
        memoryMapIndex ++;  
      }  
    }  
  
        // subpages包含了maxSubpageAllocs(2048)个PoolSubpage, 每个subpage会从chunk中分配到自己的内存段,两个subpage不会操作相同的段,此处只是初始化一个数组,还没有实际的实例化各个元素  
    subpages = newSubpageArray(maxSubpageAllocs);  
  } 

 

       //数组下标从1开始,这么做的好处是正好满足算法,素组下标*2就是其左子元素
         int memoryMapIndex = 1;
        for (int d = 0; d <= maxOrder; ++ d) { //每次处理一层,默认11层
            int depth = 1 << d;   //左移d,相当于乘以2的d次方
            for (int p = 0; p < depth; ++ p) { //这里的循环由1次,变成2次,4次,8次,16次。。2048次
                // in each level traverse left to right and set value to the depth of subtree
                
                //把每个节点设置为对应的层数
                memoryMap[memoryMapIndex] = (byte) d;
                depthMap[memoryMapIndex] = (byte) d;
                memoryMapIndex ++;
            }
        }

 

需要注意的是由于memoryMap表示的树中一个包含2 ^ maxOrde - 1个节点,因此memoryMap中的其中一个节点是无用的,为了方便后续的计算,这里将第一个节点作为无用的节点,这样从父节点计算左子结点只需要简单的*2即可,即left node id = parent id << 1。随着内存不断的分配和回收,memoryMap中的值也不停的更新,而depthMap中保存的值表示各个id对应的深度,是个固定值,初始化后不再变更。

下面看看如何向PoolChunk申请一段内存:

 

// normCapacity已经处理过  
long allocate(int normCapacity) {  
    if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize  
        // 大于等于pageSize时返回的是可分配normCapacity的节点的id  
      return allocateRun(normCapacity);  
    } else {  
        // 小于pageSize时返回的是一个特殊的long型handle  
        // handle = 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;  
        // 与上面直接返回节点id的逻辑对比可以知道,当handle<Integer.MAX_VALUE时,它表示chunk的节点id;  
        // 当handle>Integer.MAX_VALUE,他分配的是一个Subpage,节点id=memoryMapIdx, 且能得到Subpage的bitmapIdx,bitmapIdx后面会讲其用处  
      return allocateSubpage(normCapacity);  
    }  
  }  

 

allocateRun与allocateSubpage都需要查询到一个可用的id, allocateSubpage相对allocateRun多出分配PoolSubpage的步骤,下面先看看allocateRun的实现:

 

private long allocateRun(int normCapacity) {  
    // log2(val) -> Integer.SIZE - 1 - Integer.numberOfLeadingZeros(val)  
    // 如normCapacity=8192,log2(8192)=13,d=11  
        int d = maxOrder - (log2(normCapacity) - pageShifts);  
        // 通过上面一行算出对应大小需要从那一层分配,得到d后,调用allocateNode来获取对应id  
        int id = allocateNode(d);  
        if (id < 0) {  
            return id;  
        }  
        // runLenth=id所在深度占用的字节数  
        freeBytes -= runLength(id);  
        return id;  
    }  

 

allocateNode(int d)传入的参数为depth, 通过depth来搜索对应层级第一个可用的node id。下面看allocateNode的实现:

 

private int allocateNode(int d) {  
        int id = 1;  
        // 如d=11,则initial=-2048  
        int initial = - (1 << d); // has last d bits = 0 and rest all = 1  
        // value(id)=memoryMap[id]  
        //拿到第一层的值
        byte val = value(id);  
        // 第一层的节点的值大于d,表示d层及以上都不可分配,此次分配失败  
        // 这里如果不理解,拿一个小点的数组分析一下就明白了
        if (val > d) { // unusable  
            return -1;  
        }  
        
        // 这里从第二层开始从上往下查找, 一直找到指定层d  
        // val < d说明还没达到深度,还的继续
        // val = d有一种情况需要特殊注意,就是val的值会在子孙节点被分配以后变大,那么会导致在没到到达d层时,val就等于d了。 
        //比如原来第10层的val=10,子节点11层被分配后,那么val变成11,这里就相等了
        //但是仍然不能分配,因为深度不对,必须要在d层并且val还等于d的情况才能分配,所以 (id & initial) == 0 就是判断是否到达d层,只有俩者都满足了才能分配。
        while (val < d || (id & initial) == 0) {
            // 往下一层  
            id <<= 1;  
            val = value(id);  
            // 上面对一层节点的值判断已经表示有可用内存可分配,因此发现当前节点不可分配时,  
            // 直接切换到父节点的另一子节点,即id ^= 1  
            if (val > d) {
                //如果相对应位值相同,则结果为0 ,如果id为偶数相当于+1,如果奇数相当于减1,正好是兄弟节点
                id ^= 1;  
                //取兄弟节点的val继续判断
                val = value(id);  
            }  
        }  

        byte value = value(id);  
        assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",  
                value, id & initial, d);  
        // 该节点本次分配成功,将其标为不可用  
        setValue(id, unusable); // mark as unusable  
        // 更新父节点的值  
        updateParentsAlloc(id);  
        return id;  
    }  

 

 

上面有几个需要注意的点:1、分配的节点在d层;2、每一层都可能遇到节点被分配的情况,此时需要切换到父节点的另一个子节点继续往下查找;3、分配的节点需要标记为不可用,防止后面再被分配;4、分配一个节点后,其父节点的值会发生变更,并可能引起更上层的父节点的变更。下面我们来举个例子看看第4点(depth=10节点的子节点从2个可用变为1个可用的情况):

 

1)parent的depth=10,表示该层及所有子层可直接分配;

2)left节点被分配,其depth=12, 不能再被分配;

3)由于left节点已经被分配,此时parent节点已经不能直接分配,但还存在一个可分配节点right,此时parent的depth被设置为right的值11;

4)parent不可分配,则其以上的所有父节点的状态可能也会发生变化,此时需要递归的修改更上层parent的值,及指针上移后重复上面的操作。

 

private void updateParentsAlloc(int id) {  
        while (id > 1) {  
            //右移1位相当于除以2
            int parentId = id >>> 1;  

            byte val1 = value(id);  
            byte val2 = value(id ^ 1);  
            // 得到左节点和右节点较小的值赋给父节点,即两个节点只要有一个可分配,则父节点的值设为可分配的这个节点的值  
            byte val = val1 < val2 ? val1 : val2;  
            setValue(parentId, val);  
            id = parentId;  
        }  
    } 

 

到这里一个节点的分配就完成了,但是还有另一种情况,即分配的ByteBuf大小小于pageSize, 这种情况会调用allocateSubpage方法:

private long allocateSubpage(int normCapacity) {  
        int d = maxOrder; 
        //先分配一个大块
        int id = allocateNode(d);  
        if (id < 0) {  
            return id;  
        }  
        
        //本地的数组2048长度
        final PoolSubpage<T>[] subpages = this.subpages;  
        final int pageSize = this.pageSize;  
  
        freeBytes -= pageSize;  
    // 包含数据的节点在最后一层,而最后一层的左边第一个节点index=2048,因此若id=2048则subpageIdx=0,id=2049,subpageIdx=1  
    // 根据这个规律找到对应位置的PoolSubpage  
        int subpageIdx = subpageIdx(id);  
        PoolSubpage<T> subpage = subpages[subpageIdx];  
        if (subpage == null) {  
            // 如果PoolSubpage未创建则创建一个,创建时传入当前id对应的offset,pageSize,本次分配的大小normCapacity  
            subpage = new PoolSubpage<T>(this, id, runOffset(id), pageSize, normCapacity);  
            subpages[subpageIdx] = subpage;  
        } else {  
            // 已经创建则初始化数据  
            subpage.init(normCapacity);  
        }  
        // 调用此方法得到一个可以操作的handle  
        return subpage.allocate();  
    }  

 

这里可以看到,在小size的对象分配时会先分配一个PoolSubpage,最终返回一个包含该PoolSubpage信息的handle,后面的操作也是通过此handle进行操作。在看如何操作内存之前,我们先来看看分配一个节点后如何释放(比较简单,所以先讲这里),其对应的方法:

 

void free(long handle) {  
    // 传入的handle即allocate得到的handle  
        int memoryMapIdx = (int) handle;   //强转丢掉高位,得到的就是memoryMapIdx
        int bitmapIdx = (int) (handle >>> Integer.SIZE);   //把handle还原
          
        if (bitmapIdx != 0) { // free a subpage   说明这是一个subpage
            // !=0表示分配的是一个subpage  
            PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];  
            assert subpage != null && subpage.doNotDestroy;  
            if (subpage.free(bitmapIdx & 0x3FFFFFFF)) {//0x3FFFFFFF还原会真实位置
                return;  
            }  
        }  
        freeBytes += runLength(memoryMapIdx);  
        // 将节点的值改为可用,及其depth  
        setValue(memoryMapIdx, depth(memoryMapIdx));  
        // 修改其父节点的值  
        updateParentsFree(memoryMapIdx);  
    } 

 

 

这里会将节点的值改为可用,完成后该节点就可以再次被分配了,此时父节点的值还未更新,可能会导致分配节点时无法访问到此节点,因此还需要同时改变其父节点的值,

 

private void updateParentsFree(int id) {  
       int logChild = depth(id) + 1;  
       while (id > 1) {  
           int parentId = id >>> 1;  
           byte val1 = value(id);  
           byte val2 = value(id ^ 1);  
           logChild -= 1; // in first iteration equals log, subsequently reduce 1 from logChild as we traverse up  
  
           if (val1 == logChild && val2 == logChild) {  
            //当两个子节点都可分配时,该节点变回自己所在层的depth,表示该节点也可被分配  
               setValue(parentId, (byte) (logChild - 1));  
           } else {  
            // 否则与上面的updateParentsAlloc逻辑相同  
               byte val = val1 < val2 ? val1 : val2;  
               setValue(parentId, val);  
           }  
  
           id = parentId;  
       }  
   }  

 

PoolChunk本身主要是负责节点的分配与释放,因此节点的分配与释放都了解了对这个类的了解就已经差不多了。但这里还有几个方法没讲到,这几个方法是干什么用的呢,下面我们来看看:

void initBuf(PooledByteBuf<T> buf, long handle, int reqCapacity) {  
       int memoryMapIdx = (int) handle;  
       int bitmapIdx = (int) (handle >>> Integer.SIZE);  
       if (bitmapIdx == 0) {  
           // 到这里表示分配的是>=pageSize的数据  
          byte val = value(memoryMapIdx);  
           assert val == unusable : String.valueOf(val);  
          buf.init(this, handle, runOffset(memoryMapIdx), reqCapacity, runLength(memoryMapIdx));  
       } else {  
           // 到这里表示分配的是<pageSize的数据  
          initBufWithSubpage(buf, handle, bitmapIdx, reqCapacity);  
       }  
   }  
  
   void initBufWithSubpage(PooledByteBuf<T> buf, long handle, int reqCapacity) {  
initBufWithSubpage(buf, handle, (int) (handle >>> Integer.SIZE), reqCapacity);  
}  
  
private void initBufWithSubpage(PooledByteBuf<T> buf, long handle, int bitmapIdx, int reqCapacity) {  
assert bitmapIdx != 0;  
  
int memoryMapIdx = (int) handle;  
  
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];  
assert subpage.doNotDestroy;  
assert reqCapacity <= subpage.elemSize;  
  
buf.init(  
this, handle,  
runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, reqCapacity, subpage.elemSize);  
}  

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值