【Netty】ByteBuf相关(六):Subpage级别内存分配、Subpage的bitmap分析

【Netty】ByteBuf相关(六):Subpage级别内存分配、Subpage的bitmap分析

Scroll Down

文章目录


前言

本节来看看在没有命中缓存的情况下,subpage级别的内存是如何分配的,还会提及subpage级别的数据结构。命中缓存的分配流程可参考【PooledByteBufAllocator命中缓存的分配流程】

如果对本文的一些基础概念、名词不是很清楚,可以参考【ByteBuf的结构、分类、核心api简介】【内存规格、缓存&结构、chunk、arena、page、subpage等概念介绍】,本文对基础概念也不会再一一赘述了。

因为subpage在申请内存时,还是会经过page界别的,所以关于如page“树”等名词,则请参考上一节的【树图】

Netty Version:4.1.6


实验代码

TestSubpage.java

/**
 * @author WenJie
 */
public class TestSubpage{
    public static void main(String[] args) {
        // 获取PooledByteBufAllocator实例。
        PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;

        // 先创建一个ByteBuf,申请2KB内存
        ByteBuf byteBuf1 = allocator.directBuffer(2048);
        // 接着创建多个ByteBuf,同样是2KB内存。(目的是测试bitmap偏移量)
        ByteBuf byteBuf2 = allocator.directBuffer(2048);
        ByteBuf byteBuf3 = allocator.directBuffer(2048);
        ByteBuf byteBuf4 = allocator.directBuffer(2048);
        // 回收ByteBuf,这一步会缓存内存、将ByteBuf对象扔进对象池,详细等后边博客更新。
        byteBuf1.release();
        byteBuf2.release();
        byteBuf3.release();
        byteBuf4.release();
    }
}

跟进源码

源码起点

在验证subpage产生偏移量之前,我们还是得跟下byteBuf1的创建流程,了解一个page是如何切分为subpage的。

启动实验代码,直接进入io.netty.buffer.PoolArena#allocate(io.netty.buffer.PoolThreadCache, io.netty.buffer.PooledByteBuf, int)这个方法,在这之前的源码追踪请看【上一节】,为了方便,下面还是贴一下allocate方法的源码:
此处【坐标1】

    private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
        // 数值规格化,https://wenjie.store/archives/about-bytebuf-4有跟进过
        final int normCapacity = normalizeCapacity(reqCapacity);
        // 判断是够是tiny或者small规格
        if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
            int tableIdx;
            PoolSubpage<T>[] table;
            boolean tiny = isTiny(normCapacity);
            if (tiny) { // < 512
                // 尝试从tiny规格缓存拿到内存分配
                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 {
                // 尝试从small规格缓存拿到内存分配
                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];

            /**
             * 同步锁锁住head节点,防止构造的双向链表指针覆盖
             */
            synchronized (head) {
                // 获取头结点的下一个节点。
                // 如果双向链表中只有head节点,那head节点就是指向自己
                final PoolSubpage<T> s = head.next;
                // 如果双向链表不止head一个元素。
                if (s != head) {
                    assert s.doNotDestroy && s.elemSize == normCapacity;
                    // 拿到内存“偏移量”
                    long handle = s.allocate();
                    assert handle >= 0;
                    // subpage级别内存分配
                    // 实验代码创建bytebuf2的时候就进这里。
                    s.chunk.initBufWithSubpage(buf, handle, reqCapacity); 
  
                    // 计数器计数
                    if (tiny) {
                        allocationsTiny.increment();
                    } else {
                        allocationsSmall.increment();
                    }
                    return;
                }
            }
            // page、subpage级别内存分配(前提是small规格)
            // 实验代码创建bytebuf1的时候就进这里。
            allocateNormal(buf, reqCapacity, normCapacity);
            return;
        }
        // 由于我们的代码申请的内存规格是normal
        // 所以会来到一下代码。
        if (normCapacity <= chunkSize) {
            // 尝试从normal规格缓存分配内存
            // 由于我们代码是第一次,所以不会有缓存,返回false
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            // 通过Arena分配page级别内存
            allocateNormal(buf, reqCapacity, normCapacity);
        } else {
            // Huge allocations are never served via the cache so just call allocateHuge
            allocateHuge(buf, reqCapacity);
        }
    }
  • 特别留意tinySubpagePools是长度为32的数组,smallSubpagePools是长度为4的数组。

上一篇博客在讲page级别内存分配的时候,其实是跳过了上述代码的前大半段,因为当时没用上所以没讲,而现在subpage级别用上了且很关键,现在就来简单讲解下前半段。

首先来看看几个重要的参数值:

几个重要的参数.png

  • 相信有看过前言中提到的博客的人,肯定立马就能反映过来这是啥。

不知道也没关系,这里再放一次之前的图,然后来解析一下上面红框的参数都是什么意思。

Subpage级别内存结构

subpage内存结构如下图所示(和缓存用的同一张图,但底层数据结构不一样的):
subpage内存结构.png

  • 这里面的的每个红心节点均为head节点,head节点是无效节点。

假设我现在要申请16B内存,那上面源码的table就=tinySubpagePools,tableIdx就等于1了,意思就是取到tiny[1]的16B节点。

之后成功申请到内存后tinySubpagePools变为如下,此处【坐标2】
tinySubpagePools变化.png


回到我们上面的源码截图中,我们需要申请2KB内存,属于small规格,table=smallSubpagePools,tableIdx=2,其实就是取到small[2]的2KB节点,成功申请到内存后变化同tiny。


Subpage级别内存的初始化

跟进[【坐标1】]代码中的allocateNormal方法,最终来到如下方法(中间过程在上一节已经讲得够清楚了)
io.netty.buffer.PoolChunk#allocate

    long allocate(int normCapacity) {
        if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
            // page级别
            return allocateRun(normCapacity);
        } else {
            // subpage级别
            return allocateSubpage(normCapacity);
        }
    }

跟进allocateSubpage方法,此处【坐标3】
io.netty.buffer.PoolChunk#allocateSubpage

    private long allocateSubpage(int normCapacity) {
        /**
         * 同步锁锁住head节点,防止构造的双向链表指针覆盖
         */
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        synchronized (head) {
            // 获取符合内存需求的“树”的层数
            // 默认为11
            int d = maxOrder; 
            // 从“树”的第11层选择可使用的节点
            // 2048,即第11层的最左节点
            int id = allocateNode(d);
            if (id < 0) {
                return id;
            }

            final PoolSubpage<T>[] subpages = this.subpages;
            final int pageSize = this.pageSize;
            // 即便只是分配一个subpage,也要申请一个page的大小
            freeBytes -= pageSize;

            // 位运算获取偏移量
            int subpageIdx = subpageIdx(id);
            // 根据偏移量获取到对应的page
            PoolSubpage<T> subpage = subpages[subpageIdx];
            if (subpage == null) {
                // 对page进行切割(初始化),切割成多个subpage
                subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
                subpages[subpageIdx] = subpage;
            } else {
                subpage.init(head, normCapacity);
            }
            // 标记已使用的subpage,并返回偏移量
            return subpage.allocate();
        }
    }

跟进 new PoolSubpage方法看看:
io.netty.buffer.PoolSubpage#PoolSubpage(io.netty.buffer.PoolSubpage, io.netty.buffer.PoolChunk, int, int, int, int)

    PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
        // 保存所属chunk
        this.chunk = chunk;
        // 保存page在chunk中的偏移量
        this.memoryMapIdx = memoryMapIdx;
        // 保存subpage在page中的偏移量
        this.runOffset = runOffset;
        // 保存页面大小
        this.pageSize = pageSize;

        // subpage重点关注参数
        // 用于记录page中subpage的使用情况,将值转换成二进制后,0为未使用,1已使用
        bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
        // elemSize就是实验代码申请的内存大小,即2048
        init(head, elemSize);
    }
  • 关于bitmap的作用,等下就会解析。

跟进init方法:

    void init(PoolSubpage<T> head, int elemSize) {
        doNotDestroy = true;
        // 每块大小,即实验代码申请的2048
        this.elemSize = elemSize;
        if (elemSize != 0) {
            // page拆分成的份数
            // 结合实验代码这里maxNumElems = 4,每一份为2048B。
            maxNumElems = numAvail = pageSize / elemSize;
            nextAvail = 0;
            // 需要bitmap的位数/64-1
            // 结合实验代码,bitmapLength = 0;
            bitmapLength = maxNumElems >>> 6;
            // maxNumElems < 64 则 bitmapLength + 1
            if ((maxNumElems & 63) != 0) {
                bitmapLength ++;
            }

            for (int i = 0; i < bitmapLength; i ++) {
                // 初始化为0(底层是64位的0)
                bitmap[i] = 0;
            }
        }
        // 添加到双向链表
        addToPool(head);
    }

当初始化完subpage之后,我们还要将当前分配出去的subpage标记为"已使用",即更改bitmap的值。


bitmap分析

回到【坐标3】的allocateSubpage方法,找到如下语句:

...(略)
return subpage.allocate();

跟进allocate方法:
io.netty.buffer.PoolSubpage#allocate

    long allocate() {
        if (elemSize == 0) {
            return toHandle(0);
        }

        if (numAvail == 0 || !doNotDestroy) {
            return -1;
        }
        // 获取bitmap底层二进制位中非1的下标。
        // 假如bitmap[0]中的64位都被标记为1了,bitmap[1]中的第1位是0,则返回65。
        final int bitmapIdx = getNextAvail();
        // 获取可用二进制位在bitmap的下标
        // 假设bitmapIdx是65,那么它就在bitmap[1]中,以此类推。
        int q = bitmapIdx >>> 6;
        // 64位对应二进制位的下标(从0开始,即第一位为0,第二位为1)
        // 假设bitmapIdx是65,那么就返回1,表示bitmap[1]底层二进制的第一位
        int r = bitmapIdx & 63;
        assert (bitmap[q] >>> r & 1) == 0;
        // 标记page中已使用的subpage
        bitmap[q] |= 1L << r;
        
        // 此page不能容纳更多subpage了,将其从subpage池中移除
        if (-- numAvail == 0) {
            removeFromPool();
        }
        // 高位运算,返回偏移量
        return toHandle(bitmapIdx);
    }
  • 不要忘了bitmap是long数组。

下面先通过打断点直观感受下:

此page第一次分配2mb内存.png

  • 1转换成低位二进制就是:0001,表示此page已经有1/4的内存被使用了。

此page第二次分配2mb内存.png

  • 3转换成低位二进制就是:0011,表示此page已经有2/4的内存被使用了。

此page第三次分配2mb内存.png

  • 7转换成低位二进制就是:0111,表示此page已经有3/4的内存被使用了。

此page第四次分配2mb内存.png

  • 15转换成低位二进制就是:1111,表示此page已经被用完了。

结论:bitmap转化成二进制后,0表示subpage未使用,1表示subpage已使用。subpage最小单位为16kb,所以bitmap数组长度为8192(页大小)/16/64(long的字节数)。

完成了subpage的内存分配后,就剩ByteBuf的初始化了。


ByteBuf初始化

在subpage级别的内存分配下,最终都会来到initBufWithSubpage方法,这个方法的入口有很多,我就不一一赘述了,下面直接来看源码:
io.netty.buffer.PoolChunk#initBufWithSubpage(io.netty.buffer.PooledByteBuf, long, int, int)

    private void initBufWithSubpage(PooledByteBuf<T> buf, long handle, int bitmapIdx, int reqCapacity) {
        assert bitmapIdx != 0;
        // 计算chunk中page的偏移量
        int memoryMapIdx = memoryMapIdx(handle);

        // 根据偏移量获取page
        PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
        assert subpage.doNotDestroy;
        assert reqCapacity <= subpage.elemSize;

        // ByteBuf初始化
        buf.init(
            this, handle,
            runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, reqCapacity, subpage.elemSize,
            arena.parent.threadCache());
    }
  • 注意这里的handle和bitmapIdx都是经过高位运算后的,并不是前面看到的原始值。
  • runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize就是计算内存的偏移量,断点测试如下:
  • 当前page第一次分配2KB内存.png
  • 当前page第二次分配2KB内存.png
  • 第3、4次以此类推。

跟进init方法:
io.netty.buffer.PooledUnsafeDirectByteBuf#init

    @Override
    void init(PoolChunk<ByteBuffer> chunk, long handle, int offset, int length, int maxLength,
              PoolThreadCache cache) {
        super.init(chunk, handle, offset, length, maxLength, cache);
        // unsafe类型独有,保存基础内存地址。
        initMemoryAddress();
    }

继续跟进init方法:
io.netty.buffer.PooledByteBuf#init

    void init(PoolChunk<T> chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
        assert handle >= 0;
        assert chunk != null;
        // 保存所属chunk
        this.chunk = chunk;
        // 总体偏移量
        this.handle = handle;
        // 内存对象
        memory = chunk.memory;
        // subpage相对于page的偏移量
        this.offset = offset;
        // 申请的内存大小
        this.length = length;
        // 实际申请到的subpage内存大小
        this.maxLength = maxLength;
        tmpNioBuf = null;
        // 保存所属PoolThreadCache
        this.cache = cache;
    }

构建完毕后,最终返回ByteBuf对象,整个流程就完成了。


小结

  • 总体流程跟page级别的内存分配其实大同小异,最大的区别在于subpage级别需要将page拆分成相同大小的多份,并依靠额外记录相对于page的偏移量来确定subpage的内存地址。
  • page级别只需记录相对于chunk的的偏移量+内存基础地址就能定位一块连续内存。而subpage则不仅需要所属page相对于chunk的偏移量,还需要subpage本身相对于所属page的偏移量,因为每次向chunk申请内存依旧是以page为单位的。