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

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

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

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

上篇文章中我们已经大概了解了PoolChunk分配内存的原理和过程,并且知道了以下几个重要的结论:

  • PoolChunk中有两种内存单元:page和chunk
  • 一个page默认大小8K,一个chunk默认有2048个page
  • chunk中的page通过一个叫memoryMap的数组以完全二叉树的形式进行管理
  • 完全二叉树总共有4096个节点
  • 深度越深,节点越多,但是节点的容量越小
  • 一个父节点的容量等于两个子节点的容量
  • 当一个节点被分配出去了,该节点的深度要被设置为unusable
  • 递归更新被分配出去节点的父节点的深度

下面让我们了解下具体的代码实现吧,最终的内存分配就是调用的allocate方法,该方法实现如下:

long allocate(int normCapacity) {
    // 如果normCapacity大于pageSize,则说明一个SubPage无法分配,
    // 需要至少两个连续的SubPage来分配,通过allocateRun方法进行分配
    if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
        return allocateRun(normCapacity);
    // 一个SubPage就可以满足容量需求,通过allocateSubpage进行分配
    } else {
        return allocateSubpage(normCapacity);
    }
}

上一篇文章中说过,为了方便内存的分配,请求的内存大小是经过统一的normalizeCapacity方法处理过的,就是说参数normCapacity的容量总是小于pageSize或者是pageSize的整数倍。如果normCapacity小于pageSize则会在PoolArena中进行分配(但是会先在PoolThreadCache中尝试分配),否则会创建一个新的PoolChunk,然后由PoolChunk负责分配。

当normCapacity大于等于pageSize时,就调用allocateRun方法进行分配,否则调用allocateSubpage进行分配。allocateRun会分配至少一个page的内存,而allocateSubpage则只会分配一个page的内存,因此subpage只会从叶子节点上进行分配。

让我们看下allocateRun的方法,方法的注释上标注了:Allocate a run of pages (>=1),表示该方法用来分配一个连续的pages,至少一个page。代码如下:

/**
 * Allocate a run of pages (>=1)
 * 分配一个连续的pages,至少一个page
 * @param normCapacity normalized capacity
 * @return index in memoryMap
 */
private long allocateRun(int normCapacity) {
    System.out.println("allocateRun normCapacity="+normCapacity);
    // 根据容量normCapacity得到该容量的节点应该在树的第几层,计算得到d
    int d = maxOrder - (log2(normCapacity) - pageShifts);
    // 遍历节点,得到满足条件的节点,并返回该节点的id,即在memoryMap数组中的下标
    int id = allocateNode(d);
    if (id < 0) {
        return id;
    }
    freeBytes -= runLength(id);
    return id;
}

首先根据容量normCapacity得到该节点应该在树中所属的深度d,然后遍历节点,找到一个深度为d的节点,并返回该节点的id,具体的遍历方法在allocateNode中,看下源代码:

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);
    // 如果根节点的深度大于d,则说明根节点也无法分配,直接返回
    if (val > d) { // unusable
        return -1;
    }
    // 如果当前节点的深度小于d,则从树的下一层开始匹配,直到找到深度和d相等的那一层
    while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
        // 从树的下一层开始匹配 id = id*2
        id <<= 1;
        val = value(id);
        // 如果当前节点的深度大于d,则说明当前节点有子节点被分配了,则从id的兄弟节点进行匹配
        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);
    // 将当前节点id标记为不可用,防止其他线程分配
    setValue(id, unusable); // mark as unusable
    // 更新当前节点的父节点
    updateParentsAlloc(id);
    return id;
}

1、首先从树的第一个节点开始查找,如果节点的深度大于d,则说明没有符合该深度的节点了

2、否则如果节点的深度小于d,则说明该节点还有子节点可以被用来进行分配

3、从该节点的子节点开始匹配

4、如果当前节点的深度大于d,则说明该节点有子节点已经被分配了,则从兄弟节点进行匹配

5、如果找到一个节点的深度等于d,则说明该节点符合要求可以进行分配,那么将该节点标记为unusable,防止下次再被分配,并递归更新当前节点的父节点的深度

可能以上的代码理解起来比较枯燥,那我们可以通过一个具体的例子来进行分析,在分配的方法里面打印一些信息来帮助我们理解。如下面的代码,我们将申请三个ByteBuf,内存容量各不相同:

public static void pooled(){
    // 1 page
    ByteBuf buf1 = PooledByteBufAllocator.DEFAULT.heapBuffer(1025);
    // 2 pages
    ByteBuf buf2 = PooledByteBufAllocator.DEFAULT.heapBuffer(10024);
    // 2 pages
    ByteBuf buf3 = PooledByteBufAllocator.DEFAULT.heapBuffer(10035);
}

从之前的文章中我们可以知道,申请1025byte的容量需要分配1个page,而申请10024和10035都需要分配2个pages,因为他们的容量都超过了pageSize(8192)。为了更清楚的了解节点的查找匹配过程,让我们在allocateNode方法中打印一些信息来帮助我们定位,具体如下:

private int allocateNode(int d) {
    System.out.println("try to find node with depth="+d+"\n-------------------------------");
    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;
    }
    while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
        id <<= 1;
        val = value(id);
        System.out.println("check node id="+id+",depth="+val);
        if (val > d) {
            id ^= 1;
            val = value(id);
            System.out.println("check brother node id="+id+",depth="+val);
        }
    }
    byte value = value(id);
    assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
            value, id & initial, d);
    System.out.println("-------------------------------\nmatch node it="+id+",depth="+value+"\n-------------------------------");
    setValue(id, unusable); // mark as unusable
    updateParentsAlloc(id);
    return id;
}

然后让我们执行刚刚的三个ByteBuf分配的代码,执行后将打印出以下内容:

allocateSubpage normCapacity=2048
try to find node with depth=11
-------------------------------
check node id=2,depth=1
check node id=4,depth=2
check node id=8,depth=3
check node id=16,depth=4
check node id=32,depth=5
check node id=64,depth=6
check node id=128,depth=7
check node id=256,depth=8
check node id=512,depth=9
check node id=1024,depth=10
check node id=2048,depth=11
-------------------------------
match node it=2048,depth=11
-------------------------------
allocateRun normCapacity=16384
try to find node with depth=10
-------------------------------
check node id=2,depth=2
check node id=4,depth=3
check node id=8,depth=4
check node id=16,depth=5
check node id=32,depth=6
check node id=64,depth=7
check node id=128,depth=8
check node id=256,depth=9
check node id=512,depth=10
check node id=1024,depth=11
check brother node id=1025,depth=10
-------------------------------
match node it=1025,depth=10
-------------------------------
allocateRun normCapacity=16384
try to find node with depth=10
-------------------------------
check node id=2,depth=2
check node id=4,depth=3
check node id=8,depth=4
check node id=16,depth=5
check node id=32,depth=6
check node id=64,depth=7
check node id=128,depth=8
check node id=256,depth=9
check node id=512,depth=11
check brother node id=513,depth=9
check node id=1026,depth=10
-------------------------------
match node it=1026,depth=10
-------------------------------

申请的第一个ByteBuf大小为1025,超过了1024但是小于8192,所以实际分配的内存为2048。而2048的容量只需要分配一个page就够了,因此从二叉树中从第一个节点,往下一层一层的查找,找到第11层的第2048个节点就正好符合该内存申请的条件,于是将该节点分配出去,并将2048节点标记为不可用(实际的做法就是将2048节点的深度设置为12),然后将2048的所有父节点的深度更新。比如1024节点的深度从原来的10更新为11,因为1024节点的其中一个子节点2048已经被分配出去了,所以1024节点的深度应该和2049节点的深度保持一致。以此类推,512节点的深度由原来的9更新为10,一直更新到节点2为止。

当申请容量为10024的节点时,按照之前的规律,需要分配2个page。也就是需要找到一个深度为10的节点才能进行分配。仍然从第一个节点开始往下一层一层的查找,当找到512节点时发现(id & initial) == 0,也就是说512节点还是不是最终要找的节点,仍然可以往下一层进行查找。往下一层之后发现1024节点的深度为11,大于要找的深度10,所以找1024的兄弟节点1025,结果发现1025节点的深度正好是10,那么1025节点就是我们所要找的节点了。将1025节点返回,然后将他标记为不可用,并递归更新1025节点的父节点。

第三个ByteBuf对象内存的申请也跟上面两个申请的过程一样,这里就不再详细说明了。

至此PoolChunk对于节点的分配过程我们应该有了一个比较深入的了解了,下一篇文章让我们深入的了解当申请的内存小于一个page的时候,是如何进行分配的,这涉及到了PoolSubpage和PoolThreadCache类。

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

欢迎关注微信公众号

相关文章

  • Netty分享之内存竞技场(二)

    gris 转载请注明原创出处,谢谢! 上篇文章中我们已经大概了解了PoolChunk分配内存的原理和过程,并且知道...

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

    gris 转载请注明原创出处,谢谢! 上一篇文章中我们了解了在PoolChunk中分配一个或者多个page时的方法...

  • Netty分享之内存竞技场(一)

    gris 转载请注明原创出处,谢谢! 我们通常在写java程序时,不需要关心内存的分配和释放,这一切由JVM和GC...

  • Netty之ByteBuf深入分析

    netty中的PlatformDependent Netty之ByteBuf深入分析[TOC]分析思路内存与内存管...

  • 每周阅读(7/25)

    追踪 Netty 异常占用堆外内存的经验分享LeanCloud团队关于Netty堆外内存泄漏的调查 MongoDB...

  • netty内存分析

    netty内存规格 netty内存分配器类图如下 我们直接看io.netty.buffer.PooledByteB...

  • Netty之ByteBuf深入分析

    Netty之ByteBuf深入分析 [TOC] 分析思路 内存与内存管理器的抽象 ByteBuf 结构以及重要的A...

  • PooledByteBufAllocator介绍

    Netty自己实现了内存分配,把内存分配抽象出来形成了一个独立部分。Netty的内存分配借鉴了jemalloc内存...

  • Java堆外内存的回收机制

    1 堆外内存 JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,不如Netty,广泛使用...

  • 深入分析堆外内存的回收机制

    JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Netty,广泛使用了堆外内存,但...

网友评论

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

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