乐趣区

关于netty:Netty源码解析-PoolChunk实现原理jemalloc-3的算法

后面文章曾经分享了 Netty 如何援用 jemalloc 4 算法治理内存。
本文次要分享 Netty 4.1.52 之前版本中,PoolChunk 如何应用 jemalloc 3 算法治理内存。
感兴趣的同学能够比照两种算法。
源码剖析基于 Netty 4.1.29

首先阐明 PoolChunk 内存组织形式。
PoolChunk 的内存大小默认是 16M,它将内存组织成为一颗完满二叉树。
二叉树的每一层每个节点所代表的内存大小都是均等的,并且每一层节点所代表的内存大小总和加起来都是 16M。
每一层节点可分配内存是父节点的 1 /2。整颗二叉树的总层数为 12,层数从 0 开始。

示意图如下

先看一下 PoolChunk 的构造函数

PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
    unpooled = false;
    this.arena = arena;
    this.memory = memory;
    this.pageSize = pageSize;
    this.pageShifts = pageShifts;
    this.maxOrder = maxOrder;
    this.chunkSize = chunkSize;
    this.offset = offset;
    unusable = (byte) (maxOrder + 1);
    log2ChunkSize = log2(chunkSize);
    subpageOverflowMask = ~(pageSize - 1);
    freeBytes = chunkSize;

    assert maxOrder < 30 : "maxOrder should be < 30, but is:" + maxOrder;
    maxSubpageAllocs = 1 << maxOrder;

    // Generate the memory map.
    memoryMap = new byte[maxSubpageAllocs << 1];
    depthMap = new byte[memoryMap.length];
    int memoryMapIndex = 1;
    for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
        int depth = 1 << d;
        for (int p = 0; p < depth; ++ p) {
            // in each level traverse left to right and set value to the depth of subtree
            memoryMap[memoryMapIndex] = (byte) d;
            depthMap[memoryMapIndex] = (byte) d;
            memoryMapIndex ++;
        }
    }

    subpages = newSubpageArray(maxSubpageAllocs);
}

unpooled:是否应用内存池
arena:该 PoolChunk 所属的 PoolArena
memory:底层的内存块,对于堆内存,它是一个 byte 数组,对于间接内存,它是(jvm)ByteBuffer,但无论是哪种模式,其内存大小默认都是 16M。
pageSize:叶子节点大小,默认为 8192,即 8K。
maxOrder:示意二叉树最大的层数,从 0 开始。默认为 11。
chunkSize:整个 PoolChunk 的内存大小,默认为 16777216,即 16M。
offset:底层内存对齐偏移量,默认为 0。
unusable:示意节点已被调配,不必了,默认为 12。
freeBytes:闲暇内存字节数。
每个 PoolChunk 都要按内存使用率关联到一个 PoolChunkList 上,内存使用率正是通过 freeBytes 计算。
maxSubpageAllocs:叶子节点数量,默认为 2048,即 2^11。

log2ChunkSize:用于计算偏移量,默认为 24。
subpageOverflowMask:用于判断申请内存是否为 PoolSubpage,默认为 -8192。
pageShifts:用于计算分配内存所在二叉树层数,默认为 13。

memoryMap:初始化内存治理二叉树,将每一层节点值设置为层数 d。
应用数组保护二叉树,第 d 层的开始下标为 1<<d。(数组第 0 个元素不应用)。
depthMap:保留二叉树的层数,用于通过地位下标找到其在整棵树中对应的层数。
留神:depthMap 的值代表二叉树的层数,初始化后不再变动。
memoryMap 的值代表以后节点最大可申请内存块,在分配内存过程中一直变动。
节点最大可申请内存块能够通过层数 d 计算,为2 ^ (pageShifts + maxOrder - d)

PoolChunk#allocate

long allocate(int normCapacity) {if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
        return allocateRun(normCapacity);
    } else {return allocateSubpage(normCapacity);
    }
}

若申请内存大于 pageSize,调用 allocateRun 办法调配 Chunk 级别的内存。
否则调用 allocateSubpage 办法调配 PoolSubpage,再在 PoolSubpage 上调配所需内存。

PoolChunk#allocateRun

private long allocateRun(int normCapacity) {
    // #1
    int d = maxOrder - (log2(normCapacity) - pageShifts);
    // #2
    int id = allocateNode(d);
    if (id < 0) {return id;}
    // #2
    freeBytes -= runLength(id);
    return id;
}

#1 计算应该在哪层调配分配内存
maxOrder - (log2(normCapacity) - pageShifts),如 16K,即 2^14,计算结果为 10,即在 10 层调配。
#2 缩小闲暇内存字节数。

PoolChunk#allocateNode,在 d 层调配一个节点

private int allocateNode(int d) {
    int id = 1;
    int initial = - (1 << d); // has last d bits = 0 and rest all = 1
    // #1
    byte val = value(id);
    if (val > d) { // unusable
        return -1;
    }
    // #2
    while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
        // #3
        id <<= 1;
        val = value(id);
        // #4
        if (val > d) {
            // #5
            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);
    // #6
    setValue(id, unusable); // mark as unusable
    // #7
    updateParentsAlloc(id);
    return id;
}

#1 memoryMap[1] > d,第 0 层的可分配内存有余,表明该 PoolChunk 内存不能满足调配,调配失败。
#2 遍历二叉树,找到满足内存调配的节点。
val < d,即该节点内存满足调配。
id & initial = 0,即 id < 1<<d,d 层之前循环继续执行。这里并不会呈现 val > d 的场景,但会呈现 val == d 的场景,如
PoolChunk 以后可分配内存为 2M,即 memoryMap[1] = 3,这时申请 2M 内存,在 0 - 2 层,都是 val == d。可参考前面的实例。
#3 向下找到下一层下标,留神,子树左节点的下标是父节点下标的 2 倍。
#4 val > d,示意以后节点不能满足调配
#5 id ^= 1,查找同一父节点下的兄弟节点,在兄弟节点上分配内存。
id ^= 1,当 id 为偶数,即为id+=1,当 id 为奇数,即为id-=1
因为后面通过 id <<= 1 找到下一层下标都是偶数,这里等于 id+=1。
#6
因为一开始判断了 PoolChunk 内存是否足以调配,所以这里肯定能够找到一个可调配节点。
这里标注找到的节点已调配。
#7 更新找到节点的父节点最大可分配内存块大小

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

#1 向父节点遍历,直到根节点
#2 id >>> 1,找到父节点
取以后节点和兄弟节点中较小值,作为父节点的值,示意父节点最大可分配内存块大小。

如 memoryMap[1] = 0,示意最大可分配内存块为 16M。
调配 8M 后,memoryMap[1] = 1,示意以后最大可分配内存块为 8M。

上面看一则实例,大家能够联合实例了解下面的代码

内存开释

PoolChunk#free

void free(long handle) {
    // #1
    int memoryMapIdx = memoryMapIdx(handle);
    int bitmapIdx = bitmapIdx(handle);
    // #2
    if (bitmapIdx != 0) { // free a subpage
        ...
    }
    freeBytes += runLength(memoryMapIdx);
    setValue(memoryMapIdx, depth(memoryMapIdx));
    updateParentsFree(memoryMapIdx);
}

#1 获取 memoryMapIdx 和 bitmapIdx
#2 内存块在 PoolSubpage 中调配,通过 PoolSubpage 开释内存。
#3 解决到这里,就是开释 Chunk 级别的内存块了。
减少闲暇内存字节数。
设置二叉树中对应的节点为未调配
对应批改该节点的父节点。

另外,Netty 4.1.52 对 PoolArena 内存级别划分的算法也做了调整。
Netty 4.1.52 的具体算法后面文章《Netty 内存池与 PoolArena》曾经说过了,这里简略说一下 Netty 4.1.52 前的算法。
PoolArena 中将保护的内存块按大小划分为以下级别:
Tiny < 512
Small < 8192(8K)
Chunk < 16777216(16M)
Huge >= 16777216

PoolArena#tinySubpagePools,smallSubpagePools 两个数组用于保护 Tiny,Small 级别的内存块。
tinySubpagePools,32 个元素,每个数组之间差 16 个字节,大小别离为 0,16,32,48,64, … ,496
smallSubpagePools,4 个元素,每个数组之间大小翻倍,大小别离为 512,1025,2048,4096
这两个数组都是 PoolSubpage 数组,PoolSubpage 大小默认都是 8192,Tiny,Small 级别的内存都是在 PoolSubpage 上调配的。
Chunk 内存块则都是 8192 的倍数。
在 Netty 4.1.52,曾经删除了 Small 级别内存块,并引入了 SizeClasses 计算对齐内存块或计算对应的索引。
SizeClasses 默认将 16M 划分为 75 个内存块 size,内存划分更细,也能够缩小内存对齐的空间节约,更充分利用内存。

如果您感觉本文不错,欢送关注我的微信公众号,系列文章继续更新中。您的关注是我保持的能源!

退出移动版