五、netty的内存管理

        首先先来了解一对概念,堆内存和堆外内存。那么我们知道jvm其实是作为一个software跑在操作系统上的,而jvm首先会从操作系统中划出一块内存自己来管理,这块内存在jvm的管理下分成了堆,栈,计数器,方法区等等。在io操作的时候,无法直接将数据写入堆内存中,因为对于io操作,在linux下,只能由网络设备等硬件上读到内核内存中,再从堆外的内存写进堆内。为什么不能直接写入jvm的堆内存内,因为你要提供一个地址,而堆内存是会被回收的啊,鬼知道这个地址上是不是有什么重要的东西会被覆盖掉呢。那么这个方法比较垃的就是需要复制两次,必然影响效率,那么一定有人想到了,为什么不直接操作堆内核内存呢?对,没错,堆外内存就是这么产生的,也是这么用的,io直接读取堆外的内存,也就少去了复制的过程,清爽多了。但是新的问题又随之产生,堆外内存是不受jvm控制的,所以它的malloc和free都是需要手动完成的,这个过程实际上又是耗时的,所以,netty在处理的过程中,便最大化的复用了堆外内存,防止频繁的回收。

        对,这一篇博客主要就是来学习netty对堆外内存的池化处理。这个过程是比较复杂的,首先先来看一张结构图(本来是想直接盗用一篇文章的图,结果发现这个大兄弟理解和我不太一致,就照着复制了一份比较丑的)

        可以看到netty的内存池整个体系还是稍微有些庞大的,首先它的等级关系如图所示 arena > poolChunkList > poolChunk  > subPage > element(最后一个数据结构没有,只是在某些情况下subPage会被划分成多块)。这里几个数据结构之间的耦合性还是蛮强的,所以怎么讲都不好理解,所以我就随便选择一种,从小到大的来看。

        首先来看PoolSubpage,来看重要变量

private final int pageSize;
int elemSize;
private int maxNumElems;
private final long[] bitmap;


        这个pageSize便是当前page的大小,默认为8k。而elemSize是用来将subpage分成更小的多个结构(也就是图中的element)的,elemSize决定一个element的大小和当前page一共有多少个element,即maxNumElems。 因为netty对数据大小做了处理,数据大小为16byte * 2的n次方,所以不会出现类似6k这样的数据,让page无法划分。

        bitmap是用来标识page中element使用情况的,

bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64

首先,page中element的数量是不会多于  pageSize/16个的,而每个long有64位,每位便可标识一个element的使用情况,所以,用这么一个数组便可表示所有的element的使用情况。

        再看一下它的init方法

void init(PoolSubpage<T> head, int elemSize) {
    doNotDestroy = true;
    this.elemSize = elemSize;
    if (elemSize != 0) {
        maxNumElems = numAvail = pageSize / elemSize;  // 计算最大element和可用element
        nextAvail = 0;  //下一个可用element
        bitmapLength = maxNumElems >>> 6;
        if ((maxNumElems & 63) != 0) { //这里保证bitmap中至少有一个long  (maxNumElems < 64 )
            bitmapLength ++;
        }

        for (int i = 0; i < bitmapLength; i ++) {
            bitmap[i] = 0;
        }
    }
    addToPool(head);
}

        最后把它加入池子中,那这个池子实际上是在poolArena中维护的链表,我们的图中也可以看到,这个随后再说。allocate过程如下

long allocate() {
    if (elemSize == 0) {
        return toHandle(0);
    }

    if (numAvail == 0 || !doNotDestroy) {
        return -1;
    }

    final int bitmapIdx = getNextAvail();  //获取bitmap中维护的下一个可用element
    int q = bitmapIdx >>> 6;  //获取bitmap的下标
    int r = bitmapIdx & 63;   //获取在long中左偏移的位置
    assert (bitmap[q] >>> r & 1) == 0;
    bitmap[q] |= 1L << r;
    // 如果这个page已经没有空闲,则把它移除掉
    if (-- numAvail == 0) {
        removeFromPool();
    }
    
    return toHandle(bitmapIdx);
}

        再看一下获取可用element过程,实际上最后调用到的是

private int findNextAvail0(int i, long bits) {
    final int maxNumElems = this.maxNumElems;
    //这里用来记录bitmap下标
    final int baseVal = i << 6;

    for (int j = 0; j < 64; j ++) {
        if ((bits & 1) == 0) {
            // 记录他是page中第n个element
            int val = baseVal | j;
            if (val < maxNumElems) {
                return val;
            } else {
                break;
            }
        }
        bits >>>= 1;
    }
    return -1;
}

这里将element所处bitmap的位置信息存储在一个int中,所以allocate中出现了再通过反向的位运算取出对应信息。最后执行了一个toHandle方法,这里

return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;

实际上是把memoryMapIdx的信息加上了,这里的memoryMapIdx是所在chunk的信息,最终打包成一个long,那么通过这个long,就可以找到对应的chunk,subpage以及element的位置信息。对于PoolSubpage的free过程,基本上是allocate的逆过程,这里就不再说了。

        下来看PoolChunk,我们照例先来看他的重要变量,

private final byte[] memoryMap; //subpage使用情况的树状结构
private final byte[] depthMap;  //树的深度
private final PoolSubpage<T>[] subpages;  // subpages
private final int maxOrder;  //最大不超过21,决定subpage的数量
private final int chunkSize;  // 这个chunk的大小

看一下构造函数中

PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {        
    ...
    assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
    // maxSubpageAllocs 为 1<<10 即 2048
    maxSubpageAllocs = 1 << maxOrder;

    // map的大小为 4096
    memoryMap = new byte[maxSubpageAllocs << 1];
    // 树的深度 
    depthMap = new byte[memoryMap.length];
    int memoryMapIndex = 1;
    for (int d = 0; d <= maxOrder; ++ d) { 
        int depth = 1 << d;
        for (int p = 0; p < depth; ++ p) {
            // 每个节点的值等于其所在层数 从0开始
            memoryMap[memoryMapIndex] = (byte) d;
            depthMap[memoryMapIndex] = (byte) d;
            memoryMapIndex ++;
        }
    }
    // 2048个PoolSubpage
    subpages = newSubpageArray(maxSubpageAllocs);
    cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
}

        这里的核心就是,chunk中维护了一颗节点数目是subpage二倍的树来表示subpage的状态。树结构如下

                    0

             1               1

      2           2     2          2

叶子节点的数量等同于subpage的数量,数字越小,表示其拥有的叶子节点(即可分配的subpage)越多  =  2的(maxOrder - val)个。比如0表示它拥有4的叶子节点。

我们来看一下这个树的工作过程,即allocate的过程:首先会判断

normCapacity & subpageOverflowMask) != 0

要allocate的是否大于pageSize,我们就来看大于pageSize的情况吧

private long allocateRun(int normCapacity) {
    int d = maxOrder - (log2(normCapacity) - pageShifts); // 这里首先计算出来需要哪一个深度的节点
    int id = allocateNode(d);
}

        这里的d实际上表示的是他需要多少个subpage,比如d =0 ,则它需要4个subpage

下来是分配节点的过程

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;
    }
    // 这里 id < initial (id & initial) == 0 时都会成立,也就是说,可以允许tree搜索到 deep = d 那一层,如果还没有符合条件的
    // 那么下一层不可能有了
    while (val < d || (id & initial) == 0) { 
        id <<= 1;
        val = value(id);
        // 这里val > d 说明当前节点拥有的subpage少于需要的subpage
        // 那么去它的兄弟节点寻找
        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);
    //将该节点设置为unusable
    setValue(id, unusable); // mark as unusable
    updateParentsAlloc(id);
    return id;
}

        随后逐级更新它的父节点

private void updateParentsAlloc(int id) {
    while (id > 1) {
        int parentId = id >>> 1;
        byte val1 = value(id);
        byte val2 = value(id ^ 1);
        byte val = val1 < val2 ? val1 : val2;
        setValue(parentId, val);
        id = parentId;
    }
}

        我们再用刚才的tree来演示一下整个过程,malloc一个subpage,那么d = 2

                       0                                                                                           0                                                               1

             1               1                                          -->                              1             1                           -->                2            1

      2           2     2          2                                                                3   2          2    2                                          3      2    2       2

        最后init PooledByteBuf,实际上便是把对应的handle也就是chunk和subpage的信息返回给Bytebuf。

        接下来的poolChunkList相对来说就简单了,他们分组的依据是根据chunk的使用率,当chunk的使用率发生变化,它会根据是否超越边界而把chunk移动到符合条件的poolChunkList中去。

        我们最后来看一下PoolArena,惯例,先来看一下它的成员变量,

private final PoolSubpage<T>[] tinySubpagePools;   // 前文提到的,存放小于8k的对象的池子 <512byte
private final PoolSubpage<T>[] smallSubpagePools;   512byte<  < 8k
//根据使用率不同分组的chunk
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;

tinySubpagePools和smallSubpagePools会在构造函数中被初始化,其中tinySubpagePools包含32个subpage,每个subpage会作为一个header,allocate的subpage会以链表的形式跟在数组对应header后面。如,elemSize会加在tinySubpagePools[0]的next上,而header中是不保存内容的(存疑,凭什么,难道它牛逼吗?是不是因为他们是在PoolArena中,而真正存放内容的subpage要在chunk中,PoolArena中的subpage难以管理)。smallSubpagePools则是存放elemSize为512byte到8k之间的subpage。

        我们接着来看它的核心函数  allocate

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
    // 512byte一下为 16 byte的倍数  512byte以上为16*2的n次方
    final int normCapacity = normalizeCapacity(reqCapacity);
    if (isTinyOrSmall(normCapacity)) { 
        //小于8k的情况下
        int tableIdx;
        PoolSubpage<T>[] table;
        boolean tiny = isTiny(normCapacity);
        if (tiny) { // < 512
            if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                return;
            }
            tableIdx = tinyIdx(normCapacity);
            table = tinySubpagePools;
        } else {
            if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                return;
            }
            tableIdx = smallIdx(normCapacity);
            table = smallSubpagePools;
        }
        //首先先尝试从链表中取
        final PoolSubpage<T> head = table[tableIdx];

        synchronized (head) {
            final PoolSubpage<T> s = head.next;
            if (s != head) {
                assert s.doNotDestroy && s.elemSize == normCapacity;
                long handle = s.allocate();
                assert handle >= 0;
                s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
                incTinySmallAllocation(tiny);
                return;
            }
        }
        //如果链表中没有可用的poolSubpage,再通过chunk 来allocate
        synchronized (this) {
            allocateNormal(buf, reqCapacity, normCapacity);
        }
        incTinySmallAllocation(tiny);
        return;
    }
    // 大于 8k小于16M  
    if (normCapacity <= chunkSize) {
        //先通过线程缓存allocate
        if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
            return;
        }
        synchronized (this) {
            allocateNormal(buf, reqCapacity, normCapacity);
            ++allocationsNormal;
        }
    } else {
          allocateHuge(buf, reqCapacity);
    }
}

        在最后,我们可以看到PoolArena有不同的派生类DirectArena和HeapArena,HeapArean就简单了,直接基于byte[]实现。而DirectArena就要复杂的多,根据cleaner,以及系统的不同,会有不同的实现。

转载于:https://my.oschina.net/vqishiyu/blog/2998040

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值