zoukankan      html  css  js  c++  java
  • Netty源码解析 -- PoolSubpage实现原理

    前面文章说了PoolChunk如何管理Normal内存块,本文分享PoolSubpage如何管理Small内存块。
    源码分析基于Netty 4.1.52

    内存管理算法

    PoolSubpage负责管理Small内存块。一个PoolSubpage中的内存块size都相同,该size对应SizeClasses#sizeClasses表格的一个索引index。
    新创建的PoolSubpage都必须加入到PoolArena#smallSubpagePools[index]链表中。
    PoolArena#smallSubpagePools是一个PoolSubpage数组,数组中每个元素都是一个PoolSubpage链表,PoolSubpage之间可以通过next,prev组成链表。
    感兴趣的同学可以参考《内存对齐类SizeClasses》。

    注意,Small内存size并不一定小于pageSize(默认为8K)
    默认Small内存size <= 28672(28KB)
    关于Normal内存块,Small内存块,pageSize,可参考《PoolChunk实现原理》。

    PoolSubpage实际上就是PoolChunk中的一个Normal内存块,大小为其管理的内存块size与pageSize最小公倍数。
    PoolSubpage使用位图的方式管理内存块。
    PoolSubpage#bitmap是一个long数组,其中每个long元素上每个bit位都可以代表一个内存块是否使用。

    内存分配

    分配Small内存块有两个步骤

    1. PoolChunk中分配PoolSubpage。
      如果PoolArena#smallSubpagePools中已经有对应的PoolSubpage缓冲,则不需要该步骤。
    2. PoolSubpage上分配内存块

    PoolChunk#allocateSubpage

    private long allocateSubpage(int sizeIdx) {
        // #1
        PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
        synchronized (head) {
            //allocate a new run
            // #2
            int runSize = calculateRunSize(sizeIdx);
            //runSize must be multiples of pageSize
            // #3
            long runHandle = allocateRun(runSize);
            if (runHandle < 0) {
                return -1;
            }
            // #4
            int runOffset = runOffset(runHandle);
            int elemSize = arena.sizeIdx2size(sizeIdx);
    
            PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
                               runSize(pageShifts, runHandle), elemSize);
    
            subpages[runOffset] = subpage;
            // #5
            return subpage.allocate();
        }
    }
    

    #1 这里涉及修改PoolArena#smallSubpagePools中的PoolSubpage链表,需要同步操作
    #2 计算内存块size和pageSize最小公倍数
    #3 分配一个Normal内存块,作为PoolSubpage的底层内存块,大小为Small内存块size和pageSize最小公倍数
    #4 构建PoolSubpage
    runOffset,即Normal内存块偏移量,也是该PoolSubpage在整个Chunk中的偏移量
    elemSize,Small内存块size
    #5 在subpage上分配内存块

    PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int pageShifts, int runOffset, int runSize, int elemSize) {
        // #1
        this.chunk = chunk;
        this.pageShifts = pageShifts;
        this.runOffset = runOffset;
        this.runSize = runSize;
        this.elemSize = elemSize;
        bitmap = new long[runSize >>> 6 + LOG2_QUANTUM]; // runSize / 64 / QUANTUM
        init(head, elemSize);
    }
    
    void init(PoolSubpage<T> head, int elemSize) {
        doNotDestroy = true;
        if (elemSize != 0) {
            // #2
            maxNumElems = numAvail = runSize / elemSize;
            nextAvail = 0;
            bitmapLength = maxNumElems >>> 6;
            if ((maxNumElems & 63) != 0) {
                bitmapLength ++;
            }
    
            for (int i = 0; i < bitmapLength; i ++) {
                bitmap[i] = 0;
            }
        }
        // #3
        addToPool(head);
    }
    

    #1 bitmap长度为runSize / 64 / QUANTUM,从《内存对齐类SizeClasses》可以看到,runSize都是2^LOG2_QUANTUM的倍数。

    #2
    elemSize:每个内存块的大小
    maxNumElems:内存块数量
    bitmapLength:bitmap使用的long元素个数,使用bitmap中一部分元素足以管理全部内存块。
    (maxNumElems & 63) != 0,代表maxNumElems不能整除64,所以bitmapLength要加1,用于管理余下的内存块。
    #3 添加到PoolSubpage链表中

    前面分析《Netty内存池与PoolArena》中说过,在PoolArena中分配Small内存块时,首先会从PoolArena#smallSubpagePools中查找对应的PoolSubpage​。如果找到了,直接从该PoolSubpage​上分配内存。否则,分配一个Normal内存块,创建PoolSubpage​,再在上面分配内存块。

    PoolSubpage#allocate

    long allocate() {
        // #1
        if (numAvail == 0 || !doNotDestroy) {
            return -1;
        }
        // #2
        final int bitmapIdx = getNextAvail();
        // #3
        int q = bitmapIdx >>> 6;
        int r = bitmapIdx & 63;
        assert (bitmap[q] >>> r & 1) == 0;
        bitmap[q] |= 1L << r;
        // #4
        if (-- numAvail == 0) {
            removeFromPool();
        }
        // #5
        return toHandle(bitmapIdx);
    }
    

    #1 没有可用内存块,分配失败。通常PoolSubpage分配完成后会从PoolArena#smallSubpagePools中移除,不再在该PoolSubpage上分配内存,所以一般不会出现这种场景。
    #2 获取下一个可用内存块的bit下标
    #3 设置对应bit为1,即已使用
    bitmapIdx >>> 6,获取该内存块在bitmap数组中第q元素
    bitmapIdx & 63,获取该内存块是bitmap数组中第q个元素的第r个bit位
    bitmap[q] |= 1L << r,将bitmap数组中第q个元素的第r个bit位设置为1,表示已经使用
    #4 所有内存块已分配了,则将其从PoolArena中移除。
    #5 toHandle 转换为最终的handle

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

    nextAvail为初始值或free时释放的值。
    如果nextAvail存在,设置为不可用后直接返回该值。
    如果不存在,调用findNextAvail查找下一个可用内存块。

    private int findNextAvail() {
        final long[] bitmap = this.bitmap;
        final int bitmapLength = this.bitmapLength;
        // #1
        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;
    
        // #2
        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 遍历bitmap,~bits != 0,表示存在一个bit位不为1,即存在可用内存块。
    #2 遍历64个bit位,
    (bits & 1) == 0,检查最低bit位是否为0(可用),为0则返回val。
    val等于 (i << 6) | j,即i * 64 + j,该bit位在bitmap中是第几个bit位。
    bits >>>= 1,右移一位,处理下一个bit位。

    内存释放

    释放Small内存块可能有两个步骤

    1. 释放PoolSubpage的上内存块
    2. 如果PoolSubpage中的内存块已全部释放,则从Chunk中释放该PoolSubpage,同时从PoolArena#smallSubpagePools移除它。

    PoolSubpage#free

    boolean free(PoolSubpage<T> head, int bitmapIdx) {
        if (elemSize == 0) {
            return true;
        }
        // #1
        int q = bitmapIdx >>> 6;
        int r = bitmapIdx & 63;
        assert (bitmap[q] >>> r & 1) != 0;
        bitmap[q] ^= 1L << r;
    
        setNextAvail(bitmapIdx);
        // #2
        if (numAvail ++ == 0) {
            addToPool(head);
            return true;
        }
    
        // #3
        if (numAvail != maxNumElems) {
            return true;
        } else {
            // #4
            if (prev == next) {
                // Do not remove if this subpage is the only one left in the pool.
                return true;
            }
    
            // #5
            doNotDestroy = false;
            removeFromPool();
            return false;
        }
    }
    

    #1 将对应bit位设置为可以使用
    #2 在PoolSubpage的内存块全部被使用时,释放了某个内存块,这时重新加入到PoolArena中。
    #3 未完全释放,即还存在已分配内存块,返回true
    #4 逻辑到这里,是处理所有内存块已经完全释放的场景。
    PoolArena#smallSubpagePools链表组成双向链表,链表中只有head和当前PoolSubpage时,当前PoolSubpage的prev,next都指向head。
    这时当前​PoolSubpage是PoolArena中该链表最后一个PoolSubpage,不释放该PoolSubpage,以便下次申请内存时直接从该PoolSubpage上分配。
    #5 从PoolArena中移除,并返回false,这时PoolChunk会将释放对应Page节点。

    void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
        if (isSubpage(handle)) {
            // #1
            int sizeIdx = arena.size2SizeIdx(normCapacity);
            PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
    
            PoolSubpage<T> subpage = subpages[runOffset(handle)];
            assert subpage != null && subpage.doNotDestroy;
    
            synchronized (head) {
                // #2
                if (subpage.free(head, bitmapIdx(handle))) {
                    //the subpage is still used, do not free it
                    return;
                }
            }
        }
    
        // #3
        ...
    }
    

    #1
    查找head节点,同步
    #2
    调用subpage#free释放Small内存块
    如果subpage#free返回false,将继续向下执行,这时会释放PoolSubpage整个内存块,否则,不释放PoolSubpage内存块。
    #3 释放Normal内存块,就是释放PoolSubpage整个内存块。该部分内容可参考《PoolChunk实现原理》。

    如果您觉得本文不错,欢迎关注我的微信公众号,系列文章持续更新中。您的关注是我坚持的动力!

  • 相关阅读:
    设计一个圆柱体类,计算表面积及体积。建立一个半径为3、高为3.5的圆柱体,输出其表面积及体积
    写一个方法完成如下功能,判断从文本框textbox1输入的一个字符,如果是数字则求该数字的阶乘,如果是小写字条,则转换为大写,大写字符不变,结果在文本框textbox2中显示
    写一方法用来计算1+2+3+...n,其中n作为参数输入,返回值可以由方法名返回,也可以由参数返回
    winform控件记录
    写4个同名方法,实现两个整数、两个实数,一个实数一个整数,一个整数一个实数之间的求和。在主调函数中调用这4个方法计算相关的值。(方法的重载)
    写一方法计算实现任意个整数之和.在主调函数中调用该函数,实现任意个数之和。(使用params参数)
    在主函数中提示用户输入用户名和密码。另写一方法来判断用户输入是否正确。该方法分别返回一个bool类型的登录结果和和一个string类型的登录信息。如登录成功,返回true及“登录成功”,若登录失败则返回false及“用户名错误”或“密码错误”(使用out参数)
    Linux下使用Kickstart自动化安装平台架构
    Day10 多线程理论 开启线程
    关闭ipv6的方法
  • 原文地址:https://www.cnblogs.com/binecy/p/14159485.html
Copyright © 2011-2022 走看看