Netty源码分析:PoolChunk

Posted HelloWorld_EE

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty源码分析:PoolChunk相关的知识,希望对你有一定的参考价值。

Netty源码分析:PoolChunk

Chunk主要用来组织和管理多个Page的内存分配和释放。在Netty中,Chunk中的Page被构建成一颗二叉树。本博文将从源码的角度来看下PoolChunk。

1、属性和构造函数

先看下PoolChunk的属性和构造函数

    final class PoolChunk<T> // PoolChunk会涉及到具体的内存,泛型T表示byte[](堆内存)、或java.nio.ByteBuffer(堆外内存)  

        final PoolArena<T> arena;//表示该PoolChunk所属的PoolArena。  
        final T memory;// 具体用来表示内存;byte[]或java.nio.ByteBuffer。 
        final boolean unpooled;// 是否是可重用的,unpooled=false表示可重用  

        private final byte[] memoryMap;
        private final byte[] depthMap;
        private final PoolSubpage<T>[] subpages;//表示该PoolChunk所包含的PoolSubpage。也就是PoolChunk连续的可用内存。
        /** Used to determine if the requested capacity is equal to or greater than pageSize. */
        private final int subpageOverflowMask;
        private final int pageSize;//每个PoolSubpage的大小,默认为8192个字节(8K)  
        private final int pageShifts;
        private final int maxOrder;
        private final int chunkSize;
        private final int log2ChunkSize;
        private final int maxSubpageAllocs;
        /** Used to mark memory as unusable */
        private final byte unusable;

        private int freeBytes; //当前PoolChunk空闲的内存。 

        PoolChunkList<T> parent;//一个PoolChunk分配后,会根据使用率挂在PoolArena的一个PoolChunkList中
        // PoolChunk本身设计为一个链表结构
        PoolChunk<T> prev;
        PoolChunk<T> next;

        PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize) 
            unpooled = false;
            this.arena = arena;
            this.memory = memory;
            this.pageSize = pageSize;
            this.pageShifts = pageShifts;
            this.maxOrder = maxOrder;
            this.chunkSize = chunkSize;
            unusable = (byte) (maxOrder + 1);
            log2ChunkSize = log2(chunkSize);
            subpageOverflowMask = ~(pageSize - 1);
            freeBytes = chunkSize;

            assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
            maxSubpageAllocs = 1 << maxOrder;

            // Generate the memory map.
            memoryMap = new byte[maxSubpageAllocs << 1];
            depthMap = new byte[memoryMap.length];
            int memoryMapIndex = 1;
            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 = newSubpageArray(maxSubpageAllocs);
        

PoolChunk默认情况下:maxOrder = 11,即根据maxSubpageAllocs = 1 << maxOrder可得一个PoolChunk默认情况下由2^11=2048个SubPage构成,而默认情况下一个page默认大小为8k,即pageSize=8K。

重点来看下memoryMap这个字段,PoolChunk中所有的PoolSubpage都放在PoolSubpage[] subpages中,为了更好的分配,Netty用一颗平衡二叉树记录每个PoolSubpage的分配情况。假设PoolChunk由16个PoolSubpage构成(为便于分析,这里就不用默认的2048个page来进行说明chunk的结构了),那么这些PoolSubpage将会按照如下的结构组织起来。

看上面的构造函数中的两层for循环可以得到:从树根到树叶节点按每层将节点所在的(层数)依次保存在memoryMap中,即memoryMap数组中每个位置保存的是该节点所在的层数。如下就是示例的结果。

memoryMap=第0位没用到,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,//memoryMap数组元素长度为 (1<<maxOrder)>>1=32

memoryMap存储二叉树每个节点所在的层数有什么作用呢?

对于下图“圈出来”的节点,在memoryMap的索引为4,其层数是2,则:
1、如果memoryMap[4] = 2,则表示其本身到下面所有的子节点都可以被分配;
2、如果memoryMap[4] = 3, 则表示该节点下有子节点已经分配过,则该节点不能直接被分配,而其子节点中的第3层还存在未分配的节点;例如:当我们请求一个大小为4K存储区域时就会出现这种情况
3、如果memoryMap[4] = 4,则表示该节点下的2个子节点已经被分配过(但是还存在某个子节点没有分完),则该节点和两个子节点不能直接被分配,而其子节点中的第4层还存在未分配的节点。例如:当我们申请一个大小为8K和4K的存储区域是就会出现这种情况
3、如果memoryMap[4] = 5 (即总层数 + 1), 可分配的深度已经大于总层数, 则表示该节点下的所有子节点都已经被分配。例如:当我们申请一个大小为16K的存储区域时就会出现这种情况

可以这么说:如果memoryMap[i] = maxOrder+1,就表示该位置的PoolSubpage已被分配完,如果memoryMap[i] < maxOrder+1,则说明还可以分配,具体还可以分配多少,就和memoryMap[i]以及其所有子节点的值、pageSize有关。

这里假设一个PoolSubpage的大小为4K,如果申请一个大小为1K的存储区域时,该什么办呢?

对于小于一个Page的内存,Netty在Page中完成分配。每个Page会被切分成大小相同的多个存储块,存储块的大小由第一次申请的内存块大小决定。对于Page的大小为4K,第一次申请的时1K,则这个Page就会被分成4个存储块。

一个Page只能用于分配与第一次申请时大小相同的内存,例如,一个4K的Page,如果第一次分配了1K的内存,那么后面这个Page就只能继续分配1K的内存,如果有一个申请2K内存的请求,就需要在一个新的Page中进行分配。

Page中存储区域的使用状态通过一个long数组来维护,数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,1表示占用。例如:对于一个4K的Page来说如果这个Page用来分配1K的存储与区,那么long数组中就只有一个long类型的元素且这个数值的低4危用来指示4个存储区域的占用情况。

下面看看如何向PoolChunk申请一块内存区域,allocate函数的代码如下;

    long allocate(int normCapacity) 
        if ((normCapacity & subpageOverflowMask) != 0)  // >= pageSize
            return allocateRun(normCapacity);
         else 
            return allocateSubpage(normCapacity);//分析
        
     

从上面的函数可以看到根据用户申请的内存的大小,chunk采用了不同的方式,具体如下:

1、当需要分配的内存大于等于pageSize时,通过调用allocateRun函数实现内存分配。
2、当需要分配的内存小于pageSize时,通过调用allocateSubpage函数实现内存分配。

下面将会这两个函数进行分析。

1、allocateSubpage(normCapacity)

先看 allocateSubpage方法,代码如下:

    private long allocateSubpage(int normCapacity) 
        int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
        int id = allocateNode(d);//找到符合要求的节点的索引。
        if (id < 0) 
            return id;
        

        final PoolSubpage<T>[] subpages = this.subpages;
        final int pageSize = this.pageSize;

        freeBytes -= pageSize;//修改该chunk的空闲内存大小

        int subpageIdx = subpageIdx(id);//求余,得到page在subPages中的索引。
        PoolSubpage<T> subpage = subpages[subpageIdx];
        if (subpage == null) 
            subpage = new PoolSubpage<T>(this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
         else 
            subpage.init(normCapacity);
        
        return subpage.allocate();
     

前面提到过,当需要分配的内存小于pageSize时 ,会把一个page分割成多段,进行内存分配。因此,第一步就是要找到一个符合要求的节点。该功能由如下的allocateNode函数来完成。

    private int allocateNode(int d) 
        int id = 1;
        int initial = - (1 << d); // has last d bits = 0 and rest all = 1
        byte val = value(id);
        if (val > d)  // unusable
            return -1;
        
        while (val < d || (id & initial) == 0)  // id & initial == 1 << d for all ids at depth d, for < d it is 0
            id <<= 1;
            val = value(id);
            if (val > d) //当前节点不符合要求,需要到兄弟节点来寻找。
                id ^= 1;//通过异或可以找到兄弟节点
                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;
     

简单来说:该函数用于在二叉树的第d层寻找一个空闲page节点,返回的是该空闲Page节点在memoryMap的索引。 此时这里的 d = maxOrder。 代码的具体实现思路如下:

1、从根节点开始遍历,如果当前节点的val > d,说明存在子节点已经被分配了且剩余节点的内存大小不够,则此时需要到兄弟节点上继续查找。如果当前节点为根节点,根节点无兄弟节点,直接返回-1表示在该chunk不符合要求不能分配这么大的内存。

2、如果当前节点的val < d,说明该节点的内存可以被分配,则通过 id <<= 1 匹配下一层直到找到value(id) = d 且id在第d层的节点;

3、分配成功的节点需要标记为不可用,防止被再次分配,在memoryMap对应位置进行更新;

4、分配节点完成后,其父节点的状态也需要更新,并可能引起更上一层父节点的更新。父节点的val=min子节点val;

    private void updateParentsAlloc(int id) 
        while (id > 1) 
            int parentId = id >>> 1;
            byte val1 = value(id);
            byte val2 = value(id ^ 1);
            byte val = val1 < val2 ? val1 : val2;
            setValue(parentId, val);
            id = parentId;
        
    

看一个例子来说明allocateNode函数中寻找节点的算法的整个过程。

首先假设PoolChunnk由16个PoolSubpage构成,每个Page的大小为4K,那么这些PoolSubpage将会按照如下的结构组织起来。图中标出的数字为:当前节点在memoryMap中的所对应的val。

现假设用户申请一个4K的存储空间,则会将如下图所示用“红椭圆”标示的page分配出去。并进行了标识,修改了相应节点在memoryMap的值。

现在假设用户又申请一个4K的存储空间,则具体的流经过程如下图描述所示。最后的分配结果用“蓝色椭圆”进行了标识。

分配之后,修改相应节点在memoryMap的值的结果如下图所示。

以上几个图就可以很好的了解allocateNode函数中寻找节点的算法了。

回到allocateSubpage方法,在找到节点之后,即找到subpage之后由于申请的内存区域小于pageSize因此就开始调用subpage.allocate()来进行内存分配。具体细节将会在下篇博文讲解PoolSubpage的时候进行介绍,这里不进行介绍。

2、allocateRun(int normCapacity)

当需要分配的内存大于等于pageSize时,通过调用allocateRun函数实现内存分配。

    /**
     * Allocate a run of pages (>=1)
     *
     * @param normCapacity normalized capacity
     * @return index in memoryMap
     */
    private long allocateRun(int normCapacity) 
        int d = maxOrder - (log2(normCapacity) - pageShifts);//
        int id = allocateNode(d);
        if (id < 0) 
            return id;
        
        freeBytes -= runLength(id);
        return id;
    

该函数相比上面介绍的allocateSubpage方法类似且要简单,这个函数主要是利用allocateNode方法来寻找符合要求的节点即可,allocateNode方法在上面有详细的介绍。

这里主要理解下 int d = maxOrder - (log2(normCapacity) - pageShifts);代码。这行代码的作用:根据normCapacity确定需要在二叉树的d层开始节点匹配。

还是以下面这个chunk为例来进行说明。

假设PoolChunnk由16个PoolSubpage构成,则由(maxSubpageAllocs = 1 << maxOrder;)可以得到maxOrder=log(16)=4,而根据每个Page的大小为4K可以得到pageShifts=12。因此假设用户申请一个大小为16K的缓存,则maxOrder - (log2(normCapacity) - pageShifts)=2,如下图“红圈”所示的那一层。

小结

看完PoolChunk,我们需要知道的东西就两点:

1、PoolChunk是通过二叉树的形式来组织Page的。

2、当用户申请内存时,无论是大于等于PageSize的还是小于PageSize的第一步都是通过allocateNode方法来寻找符合要求的节点。其中小于pageSize的内存分配是在PoolSubpage上来进行分配的。

参考资料

1、http://www.jianshu.com/p/c4bd37a3555b

2、http://blog.csdn.net/prestigeding/article/details/54598967

3、《Netty权威指南》

以上是关于Netty源码分析:PoolChunk的主要内容,如果未能解决你的问题,请参考以下文章

Netty内存池之PoolChunk

Netty源码分析:PoolSubpage

netty内存算法小析(下)

nettybuffer源码学习2

Spring源码分析AOP源码解析(下篇)

Netty中的坑(下篇)