netty源码阅读之ByteBuf之内存page级别内存的分配

我们继续回到PoolArena的分配内存的方法:

    private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
        final int normCapacity = normalizeCapacity(reqCapacity);
        if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
            int tableIdx;
            PoolSubpage<T>[] table;
            boolean tiny = isTiny(normCapacity);
            if (tiny) { // < 512
                if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                    // was able to allocate out of the cache so move on
                    return;
                }
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else {
                if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                    // was able to allocate out of the cache so move on
                    return;
                }
                tableIdx = smallIdx(normCapacity);
                table = smallSubpagePools;
            }

            final PoolSubpage<T> head = table[tableIdx];

            /**
             * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
             * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
             */
            synchronized (head) {
                final PoolSubpage<T> s = head.next;
                if (s != head) {
                    assert s.doNotDestroy && s.elemSize == normCapacity;
                    long handle = s.allocate();
                    assert handle >= 0;
                    s.chunk.initBufWithSubpage(buf, handle, reqCapacity);

                    if (tiny) {
                        allocationsTiny.increment();
                    } else {
                        allocationsSmall.increment();
                    }
                    return;
                }
            }
            allocateNormal(buf, reqCapacity, normCapacity);
            return;
        }
        if (normCapacity <= chunkSize) {
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            allocateNormal(buf, reqCapacity, normCapacity);
        } else {
            // Huge allocations are never served via the cache so just call allocateHuge
            allocateHuge(buf, reqCapacity);
        }
    }

之前的一篇文章我们通过cache.allocateTiny()介绍了缓存的分配流程,这篇文章,我们以allocateNormal为例,介绍page级别的内存分配。

一个chunk是16M,一个page是8k,normal大小的内存,介于8k到16M之间,allocateNormal方法的名称由此来。

进入allocateNormal这个方法:

    private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
            q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
            q075.allocate(buf, reqCapacity, normCapacity)) {
            ++allocationsNormal;
            return;
        }

        // Add a new chunk.
        PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
        long handle = c.allocate(normCapacity);
        ++allocationsNormal;
        assert handle > 0;
        c.initBuf(buf, handle, reqCapacity);
        qInit.add(c);
    }

这个allocateNormal的流程,可以分为以下三段分析:

1、尝试在现有的chunk上面分配内存。

2、如果现有的chunk分配不成功,那就创建一个chunk进行内存分配

3、初始化PooledByteBuf,也就是将PooledByteBuf需要的内存指向chunk的内存。

 

一、尝试在现有的chunk上面进行分配,代码是下面这一段:

      if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
            q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
            q075.allocate(buf, reqCapacity, normCapacity)) {
            ++allocationsNormal;
            return;
        }

第一次分配内存的时候,肯定是不会走到这一步的,因为chunk还没有实例化,所以会直接走下面一步,但是我们从这里开始分析,进入q050.allocate(buf,reqCapacity,normCapacity):


    boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        if (head == null || normCapacity > maxCapacity) {
            // Either this PoolChunkList is empty or the requested capacity is larger then the capacity which can
            // be handled by the PoolChunks that are contained in this PoolChunkList.
            return false;
        }

        for (PoolChunk<T> cur = head;;) {
            long handle = cur.allocate(normCapacity);
            if (handle < 0) {
                cur = cur.next;
                if (cur == null) {
                    return false;
                }
            } else {
                cur.initBuf(buf, handle, reqCapacity);
                if (cur.usage() >= maxUsage) {
                    remove(cur);
                    nextList.add(cur);
                }
                return true;
            }
        }
    }

我们从q050的这个chunklist的第一个chunk开始进行分配,也就是调用这里long handle = cur.allocate(normCapacity);,如果handle是小于0的,那就说明分配不成功,继续寻找下一个节点进行分配;如果分配成功,就调用cur.initBuf()进行分配,也就是我们第三点要分析的,分配完之后,如果使用率超过最大使用率,就放到下一个chunklist里面去。这里最终不会调用到initBuf方法,cur==null,直接return false。

 

二、创建一个chunk进行分配

这里的代码就是一下两段:

        // Add a new chunk.
        PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
        long handle = c.allocate(normCapacity);

这一回,我们以debug的方式,分析内存的分配,首先这里是用户端代码:

public class Test {
    public static void main(String[] args) {
        int page = 1024*8;
        PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
        ByteBuf byteBuf = allocator.directBuffer(2 * page);
    }
}

debug到达这里:

查看这里的变量:

pagesize=8192,也就是我们刚刚要分配的8k内存

maxOrder=11,代表一共有11层,稍后我们会分析。

pageShifts=13,2^13=8192,稍后我们分析

chunkSize=15777216,也就是16M,也就是一个chunkSize的大小

step into进入函数:

        @Override
        protected PoolChunk<ByteBuffer> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize) {
            return new PoolChunk<ByteBuffer>(
                    this, allocateDirect(chunkSize),
                    pageSize, maxOrder, pageShifts, chunkSize);
        }

看allocateDirect(chunkSize):

        private static ByteBuffer allocateDirect(int capacity) {
            return PlatformDependent.useDirectBufferNoCleaner() ?
                    PlatformDependent.allocateDirectNoCleaner(capacity) : ByteBuffer.allocateDirect(capacity);
        }

我们看PlatformDependent.useDirectBufferNoCleaner()的值,返回true

我们知道使用的是这种规格就行了,它内部就是使用的jdk方法创建一个直接内存ByteBuffer。

然后继续进入PoolChunk的构造函数:

 这里使用的是pooled,所以unpooled为false。需要哪一块arena,哪一块memory都传入进去,之前讲解过的pageSize和pageShifts和maxOrder和chunkSIze也传进去。unusable为12,比最大的层数大1,通过这个条件,如果某个值赋值为12,我们就知道他不可用,后面分析我们就知道了。

1右移11位,maxSubpageAllocs是2048

maxSubpageAllocs右移1位是4096,所以整个memoryMap和depthMap的大小为4096大小,我们看这一段:

        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 ++;
            }
        }

我们用下面这个图辅助我们理解:

一个memoryMap是一个完全二叉树,里面有11层,对应maxOrder;memoryMapIndex就是从上往下、从左往右数下去的第几个节点;d就是树的深度,p就是树的每一层的数量。depthMap和memoryMap一样分析。最终我们会分配到一个memoryMap,一共有4096个节点,每个节点的值为树的深度,也就是memoryMap={0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,...,...11}。

然后, 每一层有代表的数据的大小:

最后我们总结一下:

参考

上面代码的逻辑及就是我们刚刚说的,最终我们会分配到一个memoryMap,一共有4096个节点,每个节点的值为树的深度,也就是memoryMap={0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,...,...11}。

 

创建完chunk之后,我们开始为chunk分配数据,也就是一开始的long handle = c.allocate(normCapacity);,断点到这里:

normCapacity大小为16M,对的。

进入:

run呢,我们大致解释为一个片段吧,一个片段其实就是page。这里我们介绍的是page的,所以进入了allocateRun里面,如果下一篇文章介绍subpage的分配,那就是进入allocateSubpage。然后进入allocateRun:

通过一定的算法,计算出d为10,也就是在第十层分配:

memoryMap的第10层为0~16k,16k~32k的块,这里我们知道,第一块就可以用来分配内存了。

我们进入allocateNode(d)吧:

d=10,我们从id为1开始找,找到合适的id,也就是memory数组里面的第id位,这个id位就是我们可以分配的内存,通过上面的图,我们可以计算出着16k的内存适合分配在memory的第1024位,也是正确的。看value(id)吧,

    private byte value(int id) {
        return memoryMap[id];
    }

也就是读出memory第id位的值。

下面一段代码:

 setValue(id, unusable); // mark as unusable

把我们这个memoryMap里面,第id位也就是1024位的值设置为unusable,也就是设置值为12,12永远小于最大层数11,所以这个做法说的过去。以后判断到位12的位,就说明使用过了,方便查找未使用的片段。

下面这段代码呢,也是挺有意思的:

 updateParentsAlloc(id);

继续深入看:

    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;
        }
    }

我们知道,memoryMap是以二叉树的形式组成的,这里做的就是将父节点也标记为使用,然后再递归调用父节点的父节点也标记为不能使用(也就是已使用),通过分析上面的代码可以很容易的得出这个结论,函数名也是这个意思。为什么要这样做呢。如果不这样做,下次要分配一块8M的内存,到了第二层发现没有被使用,直接使用了第二层的第一块,这是不符合要求的。

这个新建chunk并且分配的逻辑。

 

三、初始化PooledByteBuf

这里的作用就是将PooledByteBuf需要的内存指向chunk的内存。最后一步是从这里开始的:

c.initBuf(buf, handle, reqCapacity);

继续断点调试:

 看到这个handle为1024,就惊喜地诠释了一个chunk和一个handle可以唯一确定一块内存,handle的值其实是chunk的memoryMap数组的索引,惊不惊喜意不意外?

然后我么看bitmapIdx(handle)是做什么的:

    private static int bitmapIdx(long handle) {
        return (int) (handle >>> Integer.SIZE);
    }

Integer.SIZE就是整形数据占用空间的大小,也就是32.

handle为1024,右移32必须等于0了,只要是normal类型数据,也就是page类型的handle,右移之后一定都是等于0的。而subpage也就是我们下一篇文章要分析的,就是不等于0的。

所以我们会进入bitmap==0的这个分支,而subpage的会进入initBufWithSubpage的分支:

我们先稍微看下下面两个表达式的值:

 runOffset为0,表明没有偏移量。

runLength是16k,表明我们第1024个节点的大小为16k。

进入buf.init():

继续:

我们看到,这里就是把chunk的内存分配给bytebuf,this.chunk=chunk,代表指向哪一块chunk,handle,指向的chunk里面的哪一个位置,memory,哪一块内存,偏移量offset为0(page级别分配没有偏移量,subpage才有),大小length16384也就是16k。分配完毕回来这里:

 initMemoryAddress(),由于是unsafe的DirectByteBuf,所以要初始化memoryAddress:

    private void initMemoryAddress() {
        memoryAddress = PlatformDependent.directBufferAddress(memory) + offset;
    }

前面也已经讲解过。

最后回到这里,把分配的内存加到qInit这个chunklist里面。

    void add(PoolChunk<T> chunk) {
        if (chunk.usage() >= maxUsage) {
            nextList.add(chunk);
            return;
        }
        add0(chunk);
    }

 

发布了146 篇原创文章 · 获赞 78 · 访问量 26万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览