死磕Netty源码之内存分配详解(四)PoolArena全局内存分配

内存分配

关于我:http://huangth.com

GitHub地址:https://github.com/RobertoHuang

免责声明:本系列博客并非原创,主要借鉴和抄袭闪电侠,占小狼等知名博主博客。如有侵权请及时联系

全局分配

内存池的初始阶段线程是没有内存缓存的,所以最开始的内存分配都需要在全局分配区进行分配

全局分配区的内存构造和线程私有分配区的类似(包含Tiny、Small、Normal几种规模 计算索引的方式也都是一模一样的),无论是TinySubpagePools还是SmallSubpagePools成员在内存池初始化时是不会预置内存的,所以最开始内存分配阶段都会进入PoolArena的allocateNormal方法,代码如下

private void allocateNormal(PooledByteBuf buf, int reqCapacity, int normCapacity) {
    // 1.尝试从现有的Chunk进行分配
    if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) || q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) || q075.allocate(buf, reqCapacity, normCapacity)) {
        return;
    }

    // 2.尝试创建一个Chuank进行内存分配
    PoolChunk c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    long handle = c.allocate(normCapacity);
    assert handle > 0;
    // 3.初始化PooledByteBuf
    c.initBuf(buf, handle, reqCapacity);
    // 4.将PoolChunk添加到PoolChunkList中
    qInit.add(c);
}

分配内存时为什么选择从q050开始

1.qinit的chunk利用率低,但不会被回收
2.q075和q100由于内存利用率太高,导致内存分配的成功率大大降低,因此放到最后
3.q050保存的是内存利用率50%~100%的Chunk,这应该是个折中的选择。这样能保证Chunk的利用率都会保持在一个较高水平提高整个应用的内存利用率,并且内存利用率在50%~100%的Chunk内存分配的成功率有保障
4.当应用在实际运行过程中碰到访问高峰,这时需要分配的内存是平时的好几倍需要创建好几倍的Chunk,如果先从q0000开始,这些在高峰期创建的chunk被回收的概率会大大降低,延缓了内存的回收进度,造成内存使用的浪费

1.尝试从现有的Chunk进行分配

boolean allocate(PooledByteBuf buf, int reqCapacity, int normCapacity) {
    if (head == null || normCapacity > maxCapacity) {
        return false;
    }

    // 从head节点开始遍历
    for (PoolChunk cur = head;;) {
        // 尝试使用已有PoolChunk进行分配
        long handle = cur.allocate(normCapacity);
        if (handle < 0) {
            cur = cur.next;
            // 如果到了尾节点还没分配成功
            // 说明当前PoolChunkList无法分配内存
            if (cur == null) {
                return false;
            }
        } else {
            // 如果分配内存成功 初始化ByteBuf
            cur.initBuf(buf, handle, reqCapacity);
            // 判断PoolChunkList是否需要重新调整
            if (cur.usage() >= maxUsage) {
                remove(cur);
                nextList.add(cur);
            }
            return true;
        }
    }
}

2.尝试创建一个Chuank进行内存分配

使用newChunk(pageSize, maxOrder, pageShifts, chunkSize)对PoolChunk进行初始化,后再调用PoolChunk.allocate方法进行真正的内存分配动作,在分析这个分配动作之前先来了解一下PoolChunk。下图是PoolChunk的数据结构

死磕Netty源码之内存分配详解(四)PoolArena全局内存分配_第1张图片

PoolChunk默认由2048个Page组成(Page默认大小为8k),图中节点的值为在数组MemoryMap的下标
1、如果需要分配大小8k的内存则只需要在第11层找到第一个可用节点即可
2、如果需要分配大小16k的内存则只需要在第10层找到第一个可用节点即可
3、如果节点1024存在一个已经被分配的子节点2048则该节点不能被分配,如需要分配大小16k的内存,这个时候节点2048已被分配节点2049未被分配,就不能直接分配节点1024,因为该节点目前只剩下8k内存

PoolChunk内部会保证每次分配内存大小为8K*(2n),为了分配一个大小为ChunkSize/(2k)的节点,需要在深度为K的层从左开始匹配节点,那么如何快速的分配到指定内存?memoryMap初始化

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) {
        memoryMap[memoryMapIndex] = (byte) d;
        depthMap[memoryMapIndex] = (byte) d;
        memoryMapIndex ++;
    }
}

分配完成后 
        depthMap->[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3…]
        memoryMap->[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3…]

MemoryMap数组中每个位置保存的是该节点所在的层数,有什么作用?

对于节点512其层数是9则:
1、如果memoryMap[512]=9,则表示其本身到下面所有的子节点都可以被分配
2、如果memoryMap[512]=10,则表示节点512下有子节点已经分配过,而其子节点中的第10层还存在未分配的节点
3、如果memoryMap[512]=12(即总层数+1)可分配的深度已经大于总层数, 则表示该节点下的所有子节点都已经被分配

下面看看如何向PoolChunk申请一段内存

long allocate(int normCapacity) {
    // 当需要分配的内存大于pageSize时
    if ((normCapacity & subpageOverflowMask) != 0) {
        return allocateRun(normCapacity);
    } else {
        // 否则使用方法allocateSubpage分配内存
        return allocateSubpage(normCapacity);
    }
}

PoolChunk分配

Page级别内存分配

当需要分配的内存大于PageSize的时候,使用allocateRun()进行分配

private long allocateRun(int normCapacity) {
    // 计算出当前要分配的节点在Page树结构中的层级
    int d = maxOrder - (log2(normCapacity) - pageShifts);
    // 根据层级在Page树找出可分配内存节点进行内存分配并返回节点编号
    // 并将当前节点状态置为被使用状态 且父结构的节点设置为被使用状态
    int id = allocateNode(d);
    if (id < 0) {
        return id;
    }
    freeBytes -= runLength(id);
    return id;
}

Page级别内存分配 PooledByteBuf的初始化

void initBuf(PooledByteBuf buf, long handle, int reqCapacity) {
    int memoryMapIdx = memoryMapIdx(handle);
    int bitmapIdx = bitmapIdx(handle);
    if (bitmapIdx == 0) {
        byte val = value(memoryMapIdx);
        // 断言该节点未被使用
        assert val == unusable : String.valueOf(val);
        // Page级别PooledByteBuf初始化
        // runOffset(memoryMapIdx)计算偏移量 runLength(memoryMapIdx)获取当前节点长度
        buf.init(this, handle, runOffset(memoryMapIdx) + offset, reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
    } else {
        // SubPage级别PooledByteBuf初始化
        initBufWithSubpage(buf, handle, bitmapIdx, reqCapacity);
    }
}

void init(PoolChunk chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
    init0(chunk, handle, offset, length, maxLength, cache);
}

private void init0(PoolChunk chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
    assert handle >= 0;
    assert chunk != null;

    this.chunk = chunk;
    memory = chunk.memory;
    allocator = chunk.arena.parent;
    this.cache = cache;
    this.handle = handle;
    this.offset = offset;
    this.length = length;
    this.maxLength = maxLength;
    tmpNioBuf = null;
}

SubPage级别内存分配

当需要分配的内存小于PageSize的时候,使用allocateSubpage()进行分配

private long allocateSubpage(int normCapacity) {
    // 获取规格对应的PoolSubpage
    PoolSubpage head = arena.findSubpagePoolHead(normCapacity);
    synchronized (head) {
        int d = maxOrder; 
        // 找到一个可分配的Page的ID
        int id = allocateNode(d);
        if (id < 0) {
            return id;
        }

        final PoolSubpage[] subpages = this.subpages;
        final int pageSize = this.pageSize;

        // 修改该chunk的空闲内存大小
        freeBytes -= pageSize;

        // 获取page在subPages中的索引
        int subpageIdx = subpageIdx(id);
        PoolSubpage subpage = subpages[subpageIdx];
        if (subpage == null) {
            // 新建PoolSubpage 并添加到tinySubpagePools或smallSubpagePools中的一个缓存中
            subpage = new PoolSubpage(head, this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
        } else {
            subpage.init(head, normCapacity);
        }
        // 使用subpage分配内存(从位图中找到一个未被使用的子Page)
        return subpage.allocate();
    }
}

SubPage级别内存分配 PooledByteBuf的初始化

void initBuf(PooledByteBuf buf, long handle, int reqCapacity) {
    int memoryMapIdx = memoryMapIdx(handle);
    int bitmapIdx = bitmapIdx(handle);
    if (bitmapIdx == 0) {
        byte val = value(memoryMapIdx);
        // 断言该节点未被使用
        assert val == unusable : String.valueOf(val);
        // Page级别PooledByteBuf初始化
        // runOffset(memoryMapIdx)计算偏移量 runLength(memoryMapIdx)获取当前节点长度
        buf.init(this, handle, runOffset(memoryMapIdx) + offset, reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
    } else {
        // SubPage级别PooledByteBuf初始化
        initBufWithSubpage(buf, handle, bitmapIdx, reqCapacity);
    }
}

private void initBufWithSubpage(PooledByteBuf buf, long handle, int bitmapIdx, int reqCapacity) {
    assert bitmapIdx != 0;

    int memoryMapIdx = memoryMapIdx(handle);

    PoolSubpage subpage = subpages[subpageIdx(memoryMapIdx)];
    assert subpage.doNotDestroy;
    assert reqCapacity <= subpage.elemSize;
    // runOffset(memoryMapIdx)计算偏移量 runLength(memoryMapIdx)获取当前节点长度
    buf.init(this, handle, runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset, reqCapacity, subpage.elemSize, arena.parent.threadCache());
}

private void init0(PoolChunk chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
    assert handle >= 0;
    assert chunk != null;

    this.chunk = chunk;
    memory = chunk.memory;
    allocator = chunk.arena.parent;
    this.cache = cache;
    this.handle = handle;
    this.offset = offset;
    this.length = length;
    this.maxLength = maxLength;
    tmpNioBuf = null;
}

最终ByteBuf可根据chunk,memory ,handle ,offset 等找到对应的内存信息

将PoolChunk添加到PoolChunkList中

初始状态下所有的PoolChunkList都是空的,所以在此先创建chunk块要添加到PoolChunkList中,需要注意的是虽然都是通过qInit.add添加chunk,这并不代表chunk都会被添加到qInit这个PoolChunkList,看一下PoolChunkList的add方法就可以知道

void add(PoolChunk chunk) {
    if (chunk.usage() >= maxUsage) {
        nextList.add(chunk);
        return;
    }
    add0(chunk);
}

void add0(PoolChunk chunk) {
    chunk.parent = this;
    if (head == null) {
        head = chunk;
        chunk.prev = null;
        chunk.next = null;
    } else {
        chunk.prev = null;
        chunk.next = head;
        head.prev = chunk;
        head = chunk;
    }
}

PoolChunkList有两个重要的参数MinUsage和MaxUsage(最小/大使用率),当Chunk中内存可用率在[MinUsage,MaxUsage]区间时这个Chunk才会落到该PoolChunkList中,否则把Chunk传到下一个PoolChunkList进行检查。从这里可以看出Chunk只会被添加到内存匹配的PoolChunkList中,为了更有说服力,再看一下free方法的代码

void free(PoolChunk chunk, long handle) {  
    chunk.free(handle);  
    if (chunk.usage() < minUsage) {  
        remove(chunk);  
        if (prevList == null) {  
            // 如果前置节点为空
            // 并且使用率为0 则释放内存
            assert chunk.usage() == 0;  
            arena.destroyChunk(chunk);  
        } else {  
            prevList.add(chunk);  
        }  
    }  
} 

在释放内存时也会检查MinUsage如果不匹配传到上一个PoolChunkList进行检查,最终归还到大小跟它匹配的PoolChunkList中

PoolSubpage

关于PoolSubpage应该是在之前的博客[02.死磕Netty源码之内存分配详解(二)PoolArena内存分配结构分析]就已经提及过的,这里我们将对PoolSubpage进行详细解析。Netty提供了PoolSubpage把PoolChunk的一个Page节点8k内存划分成更小的内存段,通过对每个内存段的标记与清理标记进行内存的分配与释放,它的数据结构如下
死磕Netty源码之内存分配详解(四)PoolArena全局内存分配_第2张图片

成员变量

// 当前page所属的chunk
final PoolChunk chunk;
// 当前page在chunk中的id
private final int memoryMapIdx;
// 当前page在chunk.memory的偏移量
private final int runOffset;

// page大小
private final int pageSize;
// 每个元素是一个长整形数数记录内存页的分配信息,每个二进制位都代表页内的一个内存单元
// 当二进制位为1表示对应的内存块被分配过,第一个元素对应0-63号内存单元 第二个元素对应64-127号内存单元,第三个元素对应128-191号内存单元等等
// bitmap[0]=0b0000000...0001111表示0,1,2,3这四个内存单元都已经被分配给对应的请求了。这个bitmap用来辅助计算下一个分配块的索引也即上面的nextAvail参数
private final long[] bitmap;

// 维护链表结构
PoolSubpage prev;
PoolSubpage next;

// 当前PoolSubpage不能销毁
boolean doNotDestroy;
// 该page切分后每一段的大小
int elemSize;
// 该page包含的段数量 (maxNumElems=pageSize/elemSize)
private int maxNumElems;
private int bitmapLength;
// 下一个可用的位置
private int nextAvail;
// 内存页还能分配多少次,它的初始值等同于maxNumElems 分配一次值递减
private int numAvail;

构造方法

PoolSubpage(PoolSubpage head, PoolChunk chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
    this.chunk = chunk;
    this.memoryMapIdx = memoryMapIdx;
    this.runOffset = runOffset;
    this.pageSize = pageSize;
    // pageSize >>> 10 => pageSize/16/64
    bitmap = new long[pageSize >>> 10]; 
    init(head, elemSize);
}

默认初始化bitmap长度为8,这里解释一下为什么只需要8个元素

其实分配内存大小都是处理过的最小为16,说明一个Page可以分成8192/16=512个内存段,一个long64位可以描述64个内存段,这样只需要512/64=8long就可以描述全部内存段了

init根据当前需要分配的内存大小,确定需要多少个bitmap元素,实现如下

void init(PoolSubpage head, int elemSize) {
    doNotDestroy = true;
    this.elemSize = elemSize;
    if (elemSize != 0) {
        maxNumElems = numAvail = pageSize / elemSize;
        nextAvail = 0;
        bitmapLength = maxNumElems >>> 6;
        if ((maxNumElems & 63) != 0) {
            bitmapLength ++;
        }

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

下面通过分布申请4096和32大小的内存,说明如何确定bitmapLength的值

比如当前申请大小4096的内存maxNumElems和numAvail为2,说明一个page被拆分成2个内存段,2>>>6=02&63=0,所以bitmapLength为1,说明只需要一个long就可以描述2个内存段状态。如果当前申请大小32的内存maxNumElems和numAvail为256,说明一个page被拆分成256个内存段,256>>>6=4,说明需要4long描述256个内存段状态

内存分配

PoolSubpage的内存分配由allocate()完成

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

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

    // 找到当前page中可分配内存段的bitmapIdx
    final int bitmapIdx = getNextAvail();
    // 确定bitmap数组下标为q的long数 用来描述bitmapIdx内存段的状态
    int q = bitmapIdx >>> 6;
    // 将超出64的那一部分二进制数抹掉得到一个小于64的数
    int r = bitmapIdx & 63;
    // 断言该内存段未被使用
    assert (bitmap[q] >>> r & 1) == 0;
    // 将对应位置设置为1
    bitmap[q] |= 1L << r;

    if (-- numAvail == 0) {
        removeFromPool();
    }

    return toHandle(bitmapIdx);
}

死磕Netty源码之内存分配详解(四)PoolArena全局内存分配_第3张图片

GetNextAvail如何实现找到下一个可分配的内存段?

private int getNextAvail() {
    int nextAvail = this.nextAvail;
    // 如果nextAvail大于等于0
    // 说明nextAvail指向了下一个可分配的内存段,直接返回nextAvail值
    if (nextAvail >= 0) {
        this.nextAvail = -1;
        return nextAvail;
    }
    // 每次分配完成nextAvail被置为-1,这时只能通过方法findNextAvail重新计算出下一个可分配的内存段位置
    return findNextAvail();
}

FindNextAvail查找下一个可分配内存

private int findNextAvail() {
    final long[] bitmap = this.bitmap;
    final int bitmapLength = this.bitmapLength;
    for (int i = 0; i < bitmapLength; i ++) {
        long bits = bitmap[i];
        // ~bits != 0说明这个long所描述的64个内存段还有未分配的
        if (~bits != 0) {
            return findNextAvail0(i, bits);
        }
    }
    return -1;
}

private int findNextAvail0(int i, long bits) {
    final int maxNumElems = this.maxNumElems;
    final int baseVal = i << 6;

    for (int j = 0; j < 64; j ++) {
        // 来判断该位置是否未分配
        // 否则bits右移一位,从左到右遍历值为0的位置
        if ((bits & 1) == 0) {
            int val = baseVal | j;
            if (val < maxNumElems) {
                return val;
            } else {
                break;
            }
        }
        bits >>>= 1;
    }
    return -1;
}

至此关于Netty的内存分配告一段落……

你可能感兴趣的:(Netty源码深度解析,死磕Netty源码)