美文网首页Magic Netty
Netty分享之内存竞技场(三)

Netty分享之内存竞技场(三)

作者: 逅弈 | 来源:发表于2018-02-06 20:46 被阅读18次

    gris 转载请注明原创出处,谢谢!

    上一篇文章中我们了解了在PoolChunk中分配一个或者多个page时的方法,也就是在memoryMap中查找符合条件的节点的一个过程。
    当请求的内存小于一个pageSize时,则会创建一个PoolSubpage来进行分配。首先还是在memoryMap的叶子节点中找一个page作为要分配的PoolSubpage,然后初始化该poolSubpage,在执行init方法进行初始化的时候会将该page加入到subPagePool中去,然后在该PoolSubpage中进行内存的分配。当一个PoolSubpage已经加入到subpagePool中去了,线程下一次再来请求时则可以直接在subpagPool中进行分配。

    其实PoolSubpage跟PoolChunk很类似,一个chunk被划分成多个page,而一个page也被划分成了多个element,也就是内存段的意思。PoolChunk中管理page的是memoryMap,PoolSubpage中管理element的是bitMap。可以用下面简单的图形来表示这个关系:

    +----------------------+
    |       chunk          |
    | [p0] ... [p2047]     |
    +----------------------+
    
    +----------------------+
    |       page           |
    | [ele0] ...[elex]     |
    +----------------------+
    

    其中chunk中page的数量是确定的,但是page中element的数量需要根据eleSize来确定。

    让我们看一下PoolSubpage的初始化的代码:

    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;
        // >>>10表示除以2^10,也就是除以2^4,再除以2^6
        // 这里为什么是16,64两个数字呢,elemSize是经过normCapacity处理的数字,最小值为16;
        // 所以一个page最多可能被分成pageSize/16段内存,而一个long可以表示64个bit的状态;
        // 因此最多需要pageSize/16/64个元素就能保证所有段的状态都可以管理
        bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
        init(head, elemSize);
    }
    

    由于normCapacity的容量是经过处理过的,最小为16,所以一个page最多可以被分成pageSize/16个段,而一个long型的数字占64bit,所以bitmap最大只需要8个long型的数字就可以把所有的bit都标记出来了。

    PoolSubpage初始化完成了会调用init方法对bitmap进行初始化操作。但是init方法除了PoolSubpage初始化时调用外,当一个PoolSubpage被回收后重新进行分配时也会调用。让我们看一下init方法中做了哪些操作:

    void init(PoolSubpage<T> head, int elemSize) {
        doNotDestroy = true;
        this.elemSize = elemSize;
        if (elemSize != 0) {
            maxNumElems = numAvail = pageSize / elemSize;
            nextAvail = 0;
            // >>>6 表示除以2^6 也就是:maxNumElems/64,
            // 一个long占64个bit,所以得出需要bitmapLength个long
            bitmapLength = maxNumElems >>> 6;
            if ((maxNumElems & 63) != 0) {
                bitmapLength ++;
            }
            // 将page中划分的段都初始化为0,表示还未被分配掉
            for (int i = 0; i < bitmapLength; i ++) {
                bitmap[i] = 0;
            }
        }
        // 将该subpage加入到subpagePool中,下一次使用时可以直接从pool中获取subpage
        addToPool(head);
    }
    

    对每一个段都初始化完成之后,就需要调用allocate方法进行段的分配了,具体的分配方法如下:

    long allocate() {
        if (elemSize == 0) {
            return toHandle(0);
        }
        // 当前没有可用的element或者当前page已经被销毁了,则直接返回
        if (numAvail == 0 || !doNotDestroy) {
            return -1;
        }
        // 查找当前page中下一个可分配的内存段的index
        final int bitmapIdx = getNextAvail();
        // 得到该element段在bitmap数组中的索引下标q
        int q = bitmapIdx >>> 6;
        // 将>=64的那一部分二进制抹掉得到一个小于64的数
        int r = bitmapIdx & 63;
        // 该步表示bitmap[q]==0
        assert (bitmap[q] >>> r & 1) == 0;
        // 把第bitmap[q]标记为1,表示该element段已经被分配出去了
        bitmap[q] |= 1L << r;
    
        // 如果当前page分配完element之后没有其他可用的段了则从arena的pool中移除
        if (-- numAvail == 0) {
            removeFromPool();
        }
        return toHandle(bitmapIdx);
    }
    
    • 首先判断当前page是否可用,如果当前page中没有可用的element了或者当前page已经被销毁了,那么直接返回
    • 查找当前page中下一个可分配的内存段的index
    • 紧接着得到该element段在bitmap数组中的索引下标q
    • 把bitmap中下标为q的标记为1,表示该element段已经分配出去了
    • 如果当前page在分配完element之后,没有其他可用的段了则将其从pool中移除

    前面说了,当分配PoolSubpage时会优先从PoolThreadCache中去分配,当然刚开始的时候PoolThreadCache中是没有PoolSubpage的,当初始化好之后会把PoolSubpage加入到smallSubpagePool中去,具体的插入方法是将smallSubpagePool的head节点的next指向当前要加入的PoolSubpage。可以用下面简单的图形表示:

    
    [pool head]   [pool head]
       |           |    ^ 
       |next       |next|
       ∨           |____|
    [subpage]
    
    

    那什么时候申请PoolSubpage能从PoolThreadCache中分配到内存呢?当ByteBuf使用完了释放的时候,调用PoolArena的free方法时,会通过PoolThreadCache的add方法把当前ByteBuf所属的chunk添加到一个用MemoryRegionCache包装的queue中去。下次再申请时首先到PoolThreadCache中去分配就可以了,那怎么保证线程安全的呢?原来add添加的线程和现在get获取的线程如果不是同一个怎么办呢?
    其实PoolThreadCache是保存在一个叫PoolThreadLocalCache的FastThreadLocal类型的线程本地变量中的,每次获取或添加时总是操作的当前线程。

    以上对PoolSubpage和PoolThreadCache类的分析也完成了,但是netty的内存管理中并不仅仅包括这几个类。除了对小于pageSize的内存可以通过加入线程中的缓存来优化外,对于大于pageSize的内存netty在内存分配竞技场PoolArena中也使用了几个PoolChunkList来进行管理,主要是根据每个chunk使用的频率进行区分,保存到不同的PoolChunkList中去。chunkList和chunk的关系可以用下面简化的图形来表示:

    +------------------+           +-------+
    | [c0] <-->  [c1]  |           | chunk |
    |    chunkList     |  <--->    | List  |      
    +------------------+           +-------+
    

    PoolArena中共定义了以下几个chunkList:

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;
    

    每个chunkList有一对内存使用率的上下限指标:minUsage和maxUsage。
    以上的chunkList保存的chunk的内存使用率如下所示:

    • qInit:存储内存利用率0-25%的chunk
    • q000:存储内存利用率1-50%的chunk
    • q025:存储内存利用率25-75%的chunk
    • q050:存储内存利用率50-100%的chunk
    • q075:存储内存利用率75-100%的chunk
    • q100:存储内存利用率100%的chunk

    这些chunkList之间通过prev和next指针串成一个链,初始化PoolArena时同时初始化这些chunkList,并将它们之间的指向关系维护好了,具体代码如下:

    q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
    q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
    q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
    q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
    q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
    qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);
    
    q100.prevList(q075);
    q075.prevList(q050);
    q050.prevList(q025);
    q025.prevList(q000);
    q000.prevList(null);
    qInit.prevList(qInit);
    

    从初始化的代码可知:
    q100的next为空,prev为q075
    q075的next为q100,prev为q050
    q050的next为q075,prev为q025
    q025的next为q050,prev为q000
    q000的next为q025,prev为空
    qInit的next为q000,prev为qInit
    具体可以通过下面这张图来表示:

    
    [qInit]-->[q000]<-->[q025]<-->[q050]<-->[q075]<-->[q100]
    
    

    一个chunk从生成到消亡的过程中,不会固定在某个chunkList中,随着内存的分配和释放,根据当前的内存使用率,他会在chunkList链表中前后移动。目的就是为了增加内存分配的成功率。

    我是逅弈,如果文章对您有帮助,欢迎您点赞加关注,并欢迎您关注我的公众号:

    欢迎关注微信公众号

    相关文章

      网友评论

        本文标题:Netty分享之内存竞技场(三)

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