Netty 内存模型分析(二)PooledByteBuf 分析

前面一篇主要研究了UnpooledByteBuf 主要工作原理,本文主要探讨 PooledByteBuf 相关。
本文主要从以下几个方面梳理了PooledByteBuf:

  1. PooledByteBuf 构成
  2. PoolChunk 组织形式
  3. PoolChunk 申请大内存和小内存,以及 PoolSubpage 组装构成
  4. 池化内存PooledByteBuf 分配过程及,分配完后使用原理使用研究。

PooledByteBuf

看看其构造方法:

abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {

    private final Recycler.Handle<PooledByteBuf<T>> recyclerHandle;

    protected PoolChunk<T> chunk;   // 管理器,表明当前bytebuf属于哪个chunk
    protected long handle;   // 内存地址
    protected T memory;   // 内存方式
    protected int offset;
    protected int length;
    int maxLength;
    PoolThreadCache cache;    // 线程本地缓存
    ByteBuffer tmpNioBuf;    
    private ByteBufAllocator allocator;
	...
}

第一眼直接看他构造方法,会比较懵。

PoolChunk

PoolChunk 负责管理和分配内存,里面是以page为单位进行分配。里面以 byte[] memoryMap[] 构造了一颗平衡二叉树来对page进行维护,且可以理解为只有叶子节点代表有效内存块。
在这里插入图片描述
上图简明的一个默认的PoolChunk内存管理结构,有以下默认值:

  1. io.netty.allocator.pageSize 默认 pageSize 为 8192
  2. io.netty.allocator.maxOrder 默认最大层次是11,即4096个节点组成的完全二叉树

PoolChunk 的构造方法中,会初始化这颗二叉树:

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

PooledByteBufAllocatornewDirectBuffer 往后看,接下来看PooledChunk如何分配内存的,跟着源码调用到allocate方法
PoolChunkallocate 方法:


    boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        final long handle;
        if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
        	// 大于1页的正常分配
            handle =  allocateRun(normCapacity);
        } else {
        	// 分配小内存
            handle = allocateSubpage(normCapacity);
        }

        if (handle < 0) {
            return false;
        }
        // 初始化内存
        ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
        initBuf(buf, nioBuffer, handle, reqCapacity);
        return true;
    }
分配小内存allocateSubpage(int normCapacity)

netty如何分配内存呢?根据请求内存大小,会执行不同策略,如果请求>= 512 ,则按正常page调出。否则会将page进一步拆解,进行更细粒度内存分配。即将一个page,划分为多个连串的 PoolSubpage 数组
PoolChunkallocateSubpage 方法:

    private long allocateSubpage(int normCapacity) {
        // 根据容量,获取一个PoolSubpage 的头结点
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
        synchronized (head) {
            int id = allocateNode(d);
            if (id < 0) {
                return id;
            }
            final PoolSubpage<T>[] subpages = this.subpages;
            final int pageSize = this.pageSize;

            freeBytes -= pageSize;
			// 获取当前的subpages
            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();
        }
    }
  1. 首先根据规整后容量,从PoolArea中获取一个对应规格下的PoolSubpage的头结点。
  2. 加锁,进行分配,直接根据tree的高度,获取一个叶子节点。
  3. 维护freeBytes,即哪些内存还未分配。
  4. 根据找到的page下标,尝试去subpages中找,subpages默认为2048,即对应page数量。
  5. 初始化之后,尝试给分配内存。

上面代码有几个过程单独拎出来研究。

findSubpagePoolHead

findSubpagePoolHead 方法主要根据 分配大小搜索到这类大小对应的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 取模16后即为 tinySubpagePools 下标位置。tinySubpagePools 默认大小为32。
为什么是32,且为什么要除16呢?
因为小于512b的申请量为tiny size,而32*16=512,所以netty根据tiny size 大小,又构造出一个数组+PoolSubpage链表结构。
在这里插入图片描述
而 small size 的 smallSubpagePools 大小为 pageShift-9 ,即默认为4个,即512,1024,2048,4096大小。

allocateNode

分配结点,因为无论如何,找到一个subpage之后,也需要去由page来分配,传入d,则说明获取一个叶子节点,因为本身就是分配小内存,所以一个叶子节点够用。

    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;
    }
  1. 从第一层开始,找到第d层,再找其可用的id
  2. 设置id状态为unusable,即maxOrder+1.
  3. 调用 updateParentsAlloc 将各级祖先节点更新可用状态值。

前面知道netty管理内存池,是通过一颗完全二叉树来进行的,默认是11层,即总共大小为2048*8096=16777216(2^24) b。那么如何通过树来管理的呢?

depth = 0 时候,1个node,每个page大小为chunkSize
depth = 1 时候,2个node,每个page大小为chunkSize/2

depth = d 时候,2^d nodes,每个page大小为chunkSize/2^d

对于分配时候,如果一个节点page大小为8096,有以下情况:

  • 如果如果此时申请1b,那memoryMap[2048] = 12 (总层数+1,)说明不能分配,分配完之后,其每一层父节点一直到跟节点值,都要阶梯+1,表示其最大可分配数
  • 如果要申请8096b,则memoryMap[1024] =12,说明不能分配,同理父节点一直到更节点,将网上变更值。

总结有以下情况:

  1. memoryMap[id] = depth_of_id => 整个节点控制的所有叶子结点都是可分配的
  2. memoryMap[id] > depth_of_id => 说明其管理的最少一个page被分配,所以不能分配他,但是他的一些儿子节点能够被分配
  3. memoryMap[id] = maxOrder + 1 => 说明其不可分配,其所有孩子节点也无法分配,并且会被标记为unusable。
PoolSubpage

直接看 PoolSubpage 构造方法,new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);,前面知道了normCapacity范围,tiny size 从16到512 不等。所以normaCapacityPoolSubpage 就是叫 elemSize

    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]; // pageSize / 16 / 64
        init(head, elemSize);
    }

初始化大小位置,PoolSubpage 中分配内存,每一块并没有用一个数组进行,而是使用 nextAvail 数组指针来分配

    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;
            }
        }
        // 将当前PoolSubpage 加到head后面
        addToPool(head);
    }
  1. 使用bitmap 字段的每一位来标识数组每一块使用情况,例如 bitmap大小为8位,long 类型占用64位,当elemSize=16时候,需要 8096/16= 506个位置来标识每一个elem使用情况,8个long类型总共有64*8=512位,能够标识每一个elem使用情况。
  2. 将当前PoolSubpage 加到head 为首链表后面。
PoolSubpage的allocate

该方法原理为返回bitmap的下标返回。

    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;
        assert (bitmap[q] >>> r & 1) == 0;
        bitmap[q] |= 1L << r;
        if (-- numAvail == 0) {
            removeFromPool();
        }
        return toHandle(bitmapIdx);
    }
allocateRun(normCapacity) 分配正常容量

PoolChunkallocateRun 方法:

    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;
    }
  1. 根据传入的大小,来计算需要分配层数d
  2. 根据d仍然调用 allocateNode 分配。
  3. 更新freeBytes数量,返回id。

有细心同学可能回想,如果计算出来的d,从d层上找到的id如果不能分配了咋办?
当然这个不用担心,netty在 PoolArena 中 构造了不同PoolChunkList 的使用量,会从大到小进行选择分配,能进入allocateRun方法,在前一步已经判断过能够在当前PoolChunk中分配了。
如果最终无法分配足够内存,则返回false,而外层 allocateNormal 则会在进行 asser success 则会断言失败。

分配内存

前面都是谈如何管理内存,但是好像还没有设计到更加的底层实现,即我想看到要么就是byte[],要么就是ByteBuffer关联。
PoolChunk<T> 是一个泛型类。其内部通过 PoolArena<T> 来统一管理,PoolArena<T> 有两种内部实现:
HeapArena传入的数据为泛型数组:

        @Override
        protected PoolChunk<byte[]> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize) {
            return new PoolChunk<byte[]>(this, newByteArray(chunkSize), pageSize, maxOrder, pageShifts, chunkSize, 0);
        }

DirectArena 传入的为 ByteBuffer

        @Override
        protected PoolChunk<ByteBuffer> newChunk(int pageSize, int maxOrder,
                int pageShifts, int chunkSize) {
            if (directMemoryCacheAlignment == 0) {
                return new PoolChunk<ByteBuffer>(this,
                        allocateDirect(chunkSize), pageSize, maxOrder,
                        pageShifts, chunkSize, 0);
            }
            final ByteBuffer memory = allocateDirect(chunkSize
                    + directMemoryCacheAlignment);
            return new PoolChunk<ByteBuffer>(this, memory, pageSize,
                    maxOrder, pageShifts, chunkSize,
                    offsetCacheLine(memory));
        }

directMemoryCacheAlignment 则为直接内存的偏移量。

PooledByteBuf中使用内存

PooledUnsafeDirectByteBuf 为例,其 setBytes 方法为:

    @Override
    protected void _setByte(int index, int value) {
        UnsafeByteBufUtil.setByte(addr(index), (byte) value);
    }
  1. addr(index) 由逻辑地址转化为池中申请的内存的物理地址下标,申请的单位为PoolChunk。
  2. 将value 调用jni 方法put到对应内存地址处。

总结

本文主要从以下几个方面梳理了PooledByteBuf:

  1. PooledByteBuf 构成。
  2. PoolChunk 组织形式。
  3. PoolChunk 申请大内存和小内存,以及 PoolSubpage 组装构成。
  4. 池化内存PooledByteBuf 分配过程及,分配完后使用原理使用研究。

关注博主公众号: 六点A君。
哈哈哈,一起研究Netty:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值