关于我: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的数据结构
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);
}
}
当需要分配的内存大于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;
}
当需要分配的内存小于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 等找到对应的内存信息
初始状态下所有的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应该是在之前的博客[02.死磕Netty源码之内存分配详解(二)PoolArena内存分配结构分析]就已经提及过的,这里我们将对PoolSubpage进行详细解析。Netty提供了PoolSubpage把PoolChunk的一个Page节点8k内存划分成更小的内存段,通过对每个内存段的标记与清理标记进行内存的分配与释放,它的数据结构如下
// 当前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个内存段,一个long有64位可以描述64个内存段,这样只需要512/64=8个long就可以描述全部内存段了
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=0且2&63=0,所以bitmapLength为1,说明只需要一个long就可以描述2个内存段状态。如果当前申请大小32的内存maxNumElems和numAvail为256,说明一个page被拆分成256个内存段,256>>>6=4,说明需要4个long描述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);
}
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的内存分配告一段落……