在本篇文章中,我们将通过源码来看一下page级别的内存分配:allocateNormal()
private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
//尝试从ChunkList上进行分配
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)) {
++allocationsNormal;
return;
}
// 新建一个chunk
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
//从chunk上进行内存分配
long handle = c.allocate(normCapacity);
++allocationsNormal;
assert handle > 0;
//初始化buf
c.initBuf(buf, handle, reqCapacity);
//将这个chunk添加到chunkList上
qInit.add(c);
}
这个方法做了这么几件事:
1、尝试在现有的chunk上分配
2、创建一个chunk进行内存分配
3、初始化PooledByteBuf
现在我们就尝试拆解这几个步骤
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize) {
//首先保存传入的一些信息
unpooled = false;
this.arena = arena;
this.memory = memory;
this.pageSize = pageSize;
this.pageShifts = pageShifts;
this.maxOrder = maxOrder;
this.chunkSize = chunkSize;
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;
//生成内存和深度数组
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 ++;
}
}
//初始化subpage数组
subpages = newSubpageArray(maxSubpageAllocs);
}
在这个构造方法中,保存了一些传入的信息,初始化了根据内存段的大小初始化他们处于二叉树的第几层,之后,初始化subPage数组。
我们看一下subpage数组是如何被初始化的:
private PoolSubpage<T>[] newSubpageArray(int size) {
return new PoolSubpage[size];
}
subpages数组的大小正好等于page的个数。
long handle = c.allocate(normCapacity);
接下来我们就来看一下是如何进行内存分配的
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
首先会根据规格化后内存的大小选择subpage级别还是page级别的内存申请逻辑,在这里当然是page级别的内存申请:
private long allocateRun(int normCapacity) {
//计算出从哪一层开始查找适合的节点
int d = maxOrder - (log2(normCapacity) - pageShifts);
//开始分配内存
int id = allocateNode(d);
if (id < 0) {
return id;
}
freeBytes -= runLength(id);
return id;
}
在这方法中首先会计算出当前申请的内存大小需要从哪一层开始查找,比如,我们要申请8K的内存,当然要从最后一层开始查找了,计算出来的d正好等于maxOrder。接下来我们开始从当前层分配内存:
private int allocateNode(int d) {
int id = 1;
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
byte val = value(id);
if (val > d) { // unusable
return -1;
}
//从第一层开始向下查找,一直到我们传入的层数或者没有找到或者找的合适的内存段就跳出循环
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
id <<= 1;
val = value(id);
//如果当前节点大于最大的层数,说明当前节点的内存已经被分配,那么就换到兄弟节点上继续查找
if (val > d) {
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);
//将当前节点设置为不可用
setValue(id, unusable); // mark as unusable
//更新父节点
updateParentsAlloc(id);
return id;
}
在这个方法中首先从当前chunk的第一个节点开始向下查找,如果当前节点已经被使用了,那么换到它的兄弟节点上继续查找,注意的是,查找的最大层数就是我们传入的层数,因为那一层正好是能够分配我们申请的内存的最大层数。
这里面还有一个重要的方法就是更新父节点,因为我们查找的可以使用的节点之后,将当前节点标记为不可用,他的父节点也要更新,接下来我们看一下这段逻辑是怎么实现的:
private void updateParentsAlloc(int id) {
while (id > 1) {
//获得父节点的id
int parentId = id >>> 1;
///获得当前节点的层数
byte val1 = value(id);
//获得兄弟节点的层数
byte val2 = value(id ^ 1);
//选择一个小的层数
byte val = val1 < val2 ? val1 : val2;
//将当前父节点的层数设置为小的层数
setValue(parentId, val);
id = parentId;
}
}
其实这个更新的方法就类似于堆排序了,越往上分配的内存就越大。
long handle = c.allocate(normCapacity);
然后我们就得到了所申请的内存的位置在哪里了。现在有申请的内存了,接下来就要对buf进行初始化:
c.initBuf(buf, handle, reqCapacity);
我们进入这个方法看一下:
void initBuf(PooledByteBuf<T> buf, long handle, int reqCapacity) {
//获得page的id
int memoryMapIdx = memoryMapIdx(handle);
//获得bitmap开始的位置,由于我们的分配是page级别的,我们用不到bitmap,所以它在这里就是0
int bitmapIdx = bitmapIdx(handle);
if (bitmapIdx == 0) {
//获得当前节点对应的层数,由于已经标记为不可用了所以就是一个不要可用的层数
byte val = value(memoryMapIdx);
assert val == unusable : String.valueOf(val);
//初始化
buf.init(this, handle, runOffset(memoryMapIdx), reqCapacity, runLength(memoryMapIdx),
arena.parent.threadCache());
} else {
initBufWithSubpage(buf, handle, bitmapIdx, reqCapacity);
}
}
这个方法首先获得了我们获得内存段在chunk中的具体位置,然后根据bitmap的取值选择不同的初始化逻辑:
void init(PoolChunk<T> chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
assert handle >= 0;
assert chunk != null;
this.chunk = chunk;
this.handle = handle;
memory = chunk.memory;
this.offset = offset;
this.length = length;
this.maxLength = maxLength;
tmpNioBuf = null;
this.cache = cache;
}
初始化方法就是简单的将这段内存的信息进行赋值,就完成了初始化。
由于现在的这个chunk是创建的,需要把他放大ChunkList中:
qInit.add(c);
我们进去看一下:
void add0(PoolChunk<T> 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;
}
}
这个方法最终调用了add0方法,其实就是简单的双向链表插入一个新的节点。
重新创建一个Chunk进行内存分配的流程我们看完了,接下来我们就看一下,通过ChunkList是如何进行内存分配的:
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
//如果当前的ChunkList为或者需要分配的内存大于了最大的内存返回
if (head == null || normCapacity > maxCapacity) {
// Either this PoolChunkList is empty or the requested capacity is larger then the capacity which can
// be handled by the PoolChunks that are contained in this PoolChunkList.
return false;
}
//从头节点开始一个一个的Chunk尝试进行内存分配
for (PoolChunk<T> cur = head;;) {
long handle = cur.allocate(normCapacity);
if (handle < 0) {
//分配失败换到下一个节点
cur = cur.next;
if (cur == null) {
return false;
}
} else {
//分配成功进行buf的初始化
cur.initBuf(buf, handle, reqCapacity);
//根据当前Chunk的使用率看是否需要将他转移到其他使用率的list上
if (cur.usage() >= maxUsage) {
remove(cur);
nextList.add(cur);
}
return true;
}
}
}
这样我们Chunk的分析就结束了,逻辑不是很复杂,自己多跟几遍源码自然就懂了。