在JEMalloc分配算法文中介绍过,Chunk块随着内存使用率的变化,有六种状态:QINIT,Q0,Q25,Q50,Q75,Q100。可知,一种状态可能有多个Chunk块,Netty使用PoolChunkList
来存储这些Chunk块,它们之间的关系如下图所示:
有六种状态所以有六个PoolChunkList
,它们之间除了QINIT外形成双向链表;PoolChunkList
中的Chunk块也形成双向链表,其中头结点是双向链表的尾部,且新加入的节点也加到尾部。以Q25依次加入Chunk1,Chunk2,Chunk3为例,形成的链表如图,其中Head节点是最后加入的Chunk3节点。Chunk随着内存使用率的变化,会在PoolChunkList
中移动,初始时都在QINI,随着使用率增大,移动到Q0,Q25等;随着使用率降低,又移回Q0,当Q0中的Chunk块不再使用时,从Q0中移除。Netty对各状态内存使用率的定义稍有不同,见下表:
状态 | 最小内存使用率 | 最大内存使用率 |
---|---|---|
QINIT | 1 | 25 |
Q0 | 1 | 50 |
Q25 | 25 | 75 |
Q50 | 50 | 100 |
Q75 | 75 | 100 |
Q100 | 100 | 100 |
明白了这些,再来分析源码实现。首先看成员变量:
private final PoolArena arena; // 所属的Arena
private final int minUsage; // 状态的最小内存使用率
private final int maxUsage; // 状态的最大内存使用率
private final int maxCapacity; // 该状态下的一个Chunk可分配的最大字节数
private PoolChunk head; // head节点
private final PoolChunkList nextList; // 下一个状态
private PoolChunkList prevList; // 上一个状态
构造方法如下:
PoolChunkList(PoolArena arena, PoolChunkList nextList,
int minUsage, int maxUsage, int chunkSize) {
this.arena = arena;
this.nextList = nextList;
this.minUsage = minUsage;
this.maxUsage = maxUsage;
// 计算该状态下,一个Chunk块可以分配的最大内存
maxCapacity = calculateMaxCapacity(minUsage, chunkSize);
}
private static int calculateMaxCapacity(int minUsage, int chunkSize) {
minUsage = minUsage0(minUsage);
if (minUsage == 100) {
return 0; // Q100 不能再分配
}
// Q25中一个Chunk可以分配的最大内存为0.75 * ChunkSize
return (int) (chunkSize * (100L - minUsage) / 100L);
}
形成状态PoolChunkList
的双向链表代码在PoolArena
中,再次列出如下:
q100 = new PoolChunkList(this, null, 100, Integer.MAX_VALUE, chunkSize);
q075 = new PoolChunkList(this, q100, 75, 100, chunkSize);
q050 = new PoolChunkList(this, q075, 50, 100, chunkSize);
q025 = new PoolChunkList(this, q050, 25, 75, chunkSize);
q000 = new PoolChunkList(this, q025, 1, 50, chunkSize);
qInit = new PoolChunkList(this, q000, Integer.MIN_VALUE, 25, chunkSize);
q100.prevList(q075);
q075.prevList(q050);
q050.prevList(q025);
q025.prevList(q000);
q000.prevList(null);
qInit.prevList(qInit);
其中的prevList()
方法如下:
void prevList(PoolChunkList prevList) {
assert this.prevList == null; // 这个方法只应该在创建时调用一次
this.prevList = prevList;
}
接着分析,在PoolChunkList
中的PoolChunk
形成的双向链表的操作,代码如下:
// 增加一个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;
}
}
// 删除一个Chunk节点
private void remove(PoolChunk cur) {
if (cur == head) {
head = cur.next;
if (head != null) {
head.prev = null;
}
} else {
PoolChunk next = cur.next;
cur.prev.next = next;
if (next != null) {
next.prev = cur.prev;
}
}
}
将一个PoolChunk
加入到PoolChunkList
中的代码如下:
void add(PoolChunk chunk) {
if (chunk.usage() >= maxUsage) {
nextList.add(chunk);
return;
}
add0(chunk);
}
注意该方法实质是一个递归调用,在if
语句中会找到真正符合状态的PoolChunkList
,然后才执行add0()
加入PoolChunk
节点。随着内存使用率的增加,需要调用add()
方法将PoolChunk
向右移动到正确状态的PoolChunkList
;同理,随着内存使用率的减小,也需要一个方法将PoolChunk
向左移动到正确状态。在实现中,这个方法为move()
,名字带有歧义,忽略名字,代码如下:
private boolean move(PoolChunk chunk) {
assert chunk.usage() < maxUsage;
if (chunk.usage() < minUsage) {
return move0(chunk); // 向左移动到正确状态,递归调用
}
add0(chunk); // 到达正确状态后,加入双向链表
return true;
}
private boolean move0(PoolChunk chunk) {
if (prevList == null) {
// 此时表示chunk为Q0状态,且还需要移动,说明Chunk使用率为0
assert chunk.usage() == 0;
return false;
}
return prevList.move(chunk); // 向左移动
}
接下来,分析关键的分配过程,代码如下:
boolean allocate(PooledByteBuf buf, int reqCapacity, int normCapacity) {
// 该状态下还没有符合的Chunk块
// 申请的内存已超过一个Chunk块可以分配的最大内存
if (head == null || normCapacity > maxCapacity) {
return false;
}
// Chunk链表中寻找满足需求的Chunk块
for (PoolChunk cur = head;;) {
long handle = cur.allocate(normCapacity);
if (handle < 0) {
cur = cur.next;
if (cur == null) {
return false; // 没有满足需求
}
} else {
// 满足需求,在该Chunk块中分配
cur.initBuf(buf, handle, reqCapacity);
if (cur.usage() >= maxUsage) {
remove(cur);
nextList.add(cur); // 分配后需要向右移动至符合的状态
}
return true;
}
}
}
分配过程简单明了,释放过程也如此,代码如下:
boolean free(PoolChunk chunk, long handle) {
chunk.free(handle); // chunk释放占用的内存
if (chunk.usage() < minUsage) {
remove(chunk);
return move0(chunk); // 向左移动到符合的状态
}
return true;
}
最后,销毁PoolChunkList
的方法如下:
void destroy(PoolArena arena) {
PoolChunk chunk = head;
while (chunk != null) {
arena.destroyChunk(chunk); // 释放Chunk
chunk = chunk.next;
}
head = null; // GC回收节点
}
依次将Chunk
中的内存销毁,然后由GC回收链表节点。至此,PoolChunkList
分析完毕。
相关链接:
- JEMalloc分配算法
- PoolArena
- PoolChunk
- PoolSubpage
- PooThreadCache