Magic Netty

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

2018-02-06  本文已影响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);
}

前面说了,当分配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的内存使用率如下所示:

这些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链表中前后移动。目的就是为了增加内存分配的成功率。

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

欢迎关注微信公众号
上一篇下一篇

猜你喜欢

热点阅读