美文网首页nettynettyjava进阶干货
深入浅出Netty内存管理 PoolSubpage

深入浅出Netty内存管理 PoolSubpage

作者: 美团Java | 来源:发表于2016-08-16 21:04 被阅读6731次

    上一节中分析了如何在poolChunk中分配一块大于pageSize的内存,但在实际应用中,存在很多分配小内存的情况,如果也占用一个page,明显很浪费。针对这种情况,Netty提供了PoolSubpage把poolChunk的一个page节点8k内存划分成更小的内存段,通过对每个内存段的标记与清理标记进行内存的分配与释放。

    PoolSubpage
    final class PoolSubpage<T> {
        // 当前page在chunk中的id
        private final int memoryMapIdx; 
        // 当前page在chunk.memory的偏移量
        private final int runOffset;    
        // page大小
        private final int pageSize;
        //通过对每一个二进制位的标记来修改一段内存的占用状态
        private final long[] bitmap; 
        
        PoolSubpage<T> prev;     
        PoolSubpage<T> next;
    
        boolean doNotDestroy;    
        // 该page切分后每一段的大小
        int elemSize;   
        // 该page包含的段数量
        private int maxNumElems;        
        private int bitmapLength;
        // 下一个可用的位置
        private int nextAvail;
        // 可用的段数量
        private int numAvail;       
        ...
    }
    

    假设目前需要申请大小为4096的内存:

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

    因为4096<pageSize(8192),所以采用allocateSubpage进行内存分配,具体实现如下:

    private long allocateSubpage(int normCapacity) {
        // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
        // This is need as we may add it back and so alter the linked-list structure.
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        synchronized (head) {
            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;
    
            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、Arena负责管理PoolChunk和PoolSubpage;
    2、allocateNode负责在二叉树中找到匹配的节点,和poolChunk不同的是,只匹配叶子节点;
    3、poolChunk中维护了一个大小为2048的poolSubpage数组,分别对应二叉树中2048个叶子节点,假设本次分配到节点2048,则取出poolSubpage数组第一个元素subpage;
    4、如果subpage为空,则进行初始化,并加入到poolSubpage数组;

    subpage初始化实现如下:

    PoolSubpage(PoolSubpage<T> head, 
        PoolChunk<T> chunk, 
        int memoryMapIdx, int runOffset, 
        int pageSize, elemSize) {
    
        this.chunk = chunk;
        this.memoryMapIdx = memoryMapIdx;
        this.runOffset = runOffset;
        this.pageSize = pageSize;
        bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
        init(head, elemSize);
    }
    

    1、默认初始化bitmap长度为8,这里解释一下为什么只需要8个元素:其中分配内存大小都是处理过的,最小为16,说明一个page可以分成8192/16 = 512个内存段,一个long有64位,可以描述64个内存段,这样只需要512/64 = 8个long就可以描述全部内存段了。
    2、init根据当前需要分配的内存大小,确定需要多少个bitmap元素,实现如下:

    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;
            }
        }
        addToPool(head);
    }
    

    下面通过分布申请4096和32大小的内存,说明如何确定bitmapLength的值:

    1. 比如,当前申请大小4096的内存,maxNumElems 和 numAvail 为2,说明一个page被拆分成2个内存段,2 >>> 6 = 0,且2 & 63 != 0,所以bitmapLength为1,说明只需要一个long就可以描述2个内存段状态。
    2. 如果当前申请大小32的内存,maxNumElems 和 numAvail 为 256,说明一个page被拆分成256个内存段, 256>>> 6 = 4,说明需要4个long描述256个内存段状态。

    下面看看subpage是如何进行内存分配的:

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

    1、方法getNextAvail负责找到当前page中可分配内存段的bitmapIdx;
    2、q = bitmapIdx >>> 6,确定bitmap数组下标为q的long数,用来描述 bitmapIdx 内存段的状态;
    3、bitmapIdx & 63将超出64的那一部分二进制数抹掉,得到一个小于64的数r;
    4、bitmap[q] |= 1L << r将对应位置q设置为1;

    如果以上描述不直观的话,下面换一种方式解释,假设需要分配大小为128的内存,这时page会拆分成64个内存段,需要1个long类型的数字描述这64个内存段的状态,所以bitmap数组只会用到第一个元素。

    状态转换

    getNextAvail如何实现找到下一个可分配的内存段?

    private int getNextAvail() {
        int nextAvail = this.nextAvail;
        if (nextAvail >= 0) {
            this.nextAvail = -1;
            return nextAvail;
        }
        return findNextAvail();
    }
    

    1、如果nextAvail大于等于0,说明nextAvail指向了下一个可分配的内存段,直接返回nextAvail值;
    2、每次分配完成,nextAvail被置为-1,这时只能通过方法findNextAvail重新计算出下一个可分配的内存段位置。

    private int findNextAvail() {
        final long[] bitmap = this.bitmap;
        final int bitmapLength = this.bitmapLength;
        for (int i = 0; i < bitmapLength; i ++) {
            long bits = bitmap[i];
            if (~bits != 0) {
                return findNextAvail0(i, bits);
            }
        }
        return -1;
    }
    
    private int findNextAvail0(int i, long bits) {
        final int maxNumElems = this.maxNumElems;
        final int baseVal = i << 6;
        for (int j = 0; j < 64; j ++) {
            if ((bits & 1) == 0) {
                int val = baseVal | j;
                if (val < maxNumElems) {
                    return val;
                } else {
                    break;
                }
            }
            bits >>>= 1;
        }
        return -1;
    }
    

    1、~bits != 0说明这个long所描述的64个内存段还有未分配的;
    2、(bits & 1) == 0 用来判断该位置是否未分配,否则bits又移一位,从左到右遍历值为0的位置;

    至此,subpage内存段已经分配完成。

    END。
    我是占小狼。
    在魔都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
    如果读完觉得有收获的话,记得关注和点赞哦。
    非要打赏的话,我也是不会拒绝的。

    相关文章

      网友评论

      • ShallWei:写的很棒,正好在看netty源码部分,看了您的博客加快了理解
      • 福克斯记:描述4096和32内存分配两段写反了

      • 28e961cfc2d0:相当深入

      本文标题:深入浅出Netty内存管理 PoolSubpage

      本文链接:https://www.haomeiwen.com/subject/llspsttx.html