Netty解析二十三:Netty内存分配PoolSubpage
上一篇解析了Netty内存分配的伙伴模型和PoolChunk的原理,如果需要分配的内存大于等于pageSize则会直接通过Chunk进行分配,不过不会产生Page对象;如果小于pageSize也就是分为tiny/small会通过Chunk然后分配Subpage,由Subpage来管理具体内存。
PoolSubpage解析
Page按固定大小(elemSize)切分一小块一小块的,这样的page叫做SubPage。在chunk中Page是一个虚拟的概念,由二叉树控制,当需要的内存空间小于PageSize的时候就需要对Page进行切割,此时就需要一个对象来管理这些被切割内存的分配。
分配过程是:chunk.allocate->chunk.allocateSubpage
继承关系以及成员变量
1 | final class PoolSubpage<T> implements PoolSubpageMetric { |
构造器
PoolSubpage也有两个构造器,一个是用于构造PoolSubpage链表的表头节点的,另一个是正常构造器。
链表头节点构造器1
2
3
4
5
6
7
8PoolSubpage(int pageSize) {
chunk = null;
memoryMapIdx = -1;
runOffset = -1;
elemSize = -1;
this.pageSize = pageSize;
bitmap = null;
}
普通构造器1
2
3
4
5
6
7
8
9
10
11PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
// 一位代表一个内存块的分配情况,long是64位所以代表64块,而pageSize是通过PoolArena#normalizeCapacity方法优化过的,最小值为16
// 所以最大的bitmap就是 pageSize / 16 / 64
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
// 初始化的核心方法
init(head, elemSize);
}
初始init
在chunk分配Subpage对象的可以看到init方法和allocate方法是核心。1
2
3
4
5
6
7if (subpage == null) {
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
return subpage.allocate();
1 | void init(PoolSubpage<T> head, int elemSize) { |
添加到池中addToPool
PoolArena会管理chunk和Subpage,PoolArena中有两个PoolSubpage链表,一个存tiny大小的SubPage,另一个存small大小的Subpage。所谓的添加到池中就是将当前Subpage插入到链表的head后面一位,这样PoolArena就能通过链表来管理者SubPage。
1 | private void addToPool(PoolSubpage<T> head) { |
分配内存allocate
分配内存的逻辑计算计算哪个块可用bitmapIdx,然将对于的位置1。
1 | long allocate() { |
- 如果块大小为0,则直接分配第一块;
- 如果没有可用的块或者已经销毁了,直接返回-1;
- 获取下一个可用块的bitmapIdx;
- 校验bitmapIdx所在的块可用;
- 可用块数numAvail减一;
- 如果numAvail为0了,则将当前Subpage移出pool(链表);
- 计算这个块的handle数(0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx);
释放内存free
- 如果这个Subpage还再被使用则返回true;
- 如果这个Subpage没有被它的chunk使用且被释放了则返回false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
// 校验这个块是不是未释放
assert (bitmap[q] >>> r & 1) != 0;
// 块所在的位置0
bitmap[q] ^= 1L << r;
// 将下一个可用的块设置为刚释放的这个块
setNextAvail(bitmapIdx);
// 可用块数加一
if (numAvail ++ == 0) {
// 如果之前可用块数为0,则将当前节点加入链表
addToPool(head);
return true;
}
// 不是全部块都可用
if (numAvail != maxNumElems) {
return true;
} else {
// Subpage not in use (numAvail == maxNumElems)
// 如果这个节点前面只有一个节点就保留当前节点
if (prev == next) {
// Do not remove if this subpage is the only one left in the pool.
return true;
}
// 如果在这个Subpage前面还有Subpage那就移出池
// Remove this subpage from the pool if there are other subpages left in the pool.
doNotDestroy = false;
removeFromPool();
return false;
}
}
- 如果块大小为0,则返回true;
- 校验这个块是不是未释放;
- 块所在的位(bitmap)置0;
- 将下一个可用的块设置为刚释放的这个块(nextAvail = bitmapIdx);
- 可用块数加一;
- 如果之前可用块数为0,则将当前节点加入链表,返回true;
- 如果还有块在被使用,则返回true;
- 全部块都可用:
- 如果当前节点的前一个节点和后一个节点都相等(除头节点就只有一个节点),则返回true;
- doNotDestroy置为false;
- 将当前节点移出pool;
- 返回false;
工具方法
生成带page的序号toHandle()
1 | private long toHandle(int bitmapIdx) { |
获取下一个可用的块getNextAvail()
getNextAvail有两种情况:
- nextAvail存有值(>=),那么这个位置就是空闲的,返回这个值,并置为this.nextAvail = -1;
- 使用findNextAvail方法遍历bitmap数组查找那个位置是空闲的。
第一种情况是和内存释放free()方法相匹配的,释放内存块后会将对应的位置(bitmapIdx)设置为下一个可用的位置nextAvail,这样节约零碎空间遍历的时间成本。
第二种是常规的遍历查找,相对来说就会花时间。
1 | private int getNextAvail() { |
查找下一个可用的块findNextAvail()
findNextAvail确定bitmap数组哪一位还有空余空间,然后交由findNextAvail0查找哪一位还有未分配。1
2
3
4
5
6
7
8
9
10
11
12
13
14private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
// 遍历bitmap
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
// 没有全部为1,即还用空
if (~bits != 0) {
// 查找到哪一个
return findNextAvail0(i, bits);
}
}
return -1;
}
findNextAvail0遍历64位字节,确定哪一位有空余,遍历从低位开始。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
// i*64,表示数组中位置
final int baseVal = i << 6;
// 遍历每一位,确定位置
for (int j = 0; j < 64; j ++) {
if ((bits & 1) == 0) {
// 加上位的位置
int val = baseVal | j;
if (val < maxNumElems) {
return val;
} else {
break;
}
}
// 每次丢弃一位
bits >>>= 1;
}
return -1;
}
removeFromPool()
在Subpage的所有块都被使用时会将当前Subpage移出链表。1
2
3
4
5
6
7private void removeFromPool() {
assert prev != null && next != null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
}