逅弈 转载请注明原创出处,谢谢!
上一篇文章中我们了解了在PoolChunk中分配一个或者多个page时的方法,也就是在memoryMap中查找符合条件的节点的一个过程。
当请求的内存小于一个pageSize时,则会创建一个PoolSubpage来进行分配。首先还是在memoryMap的叶子节点中找一个page作为要分配的PoolSubpage,然后初始化该poolSubpage,在执行init方法进行初始化的时候会将该page加入到subPagePool中去,然后在该PoolSubpage中进行内存的分配。当一个PoolSubpage已经加入到subpagePool中去了,线程下一次再来请求时则可以直接在subpagPool中进行分配。
其实PoolSubpage跟PoolChunk很类似,一个chunk被划分成多个page,而一个page也被划分成了多个element,也就是内存段的意思。PoolChunk中管理page的是memoryMap,PoolSubpage中管理element的是bitMap。可以用下面简单的图形来表示这个关系:
+----------------------+
| chunk |
| [p0] ... [p2047] |
+----------------------+
+----------------------+
| page |
| [ele0] ...[elex] |
+----------------------+
其中chunk中page的数量是确定的,但是page中element的数量需要根据eleSize来确定。
让我们看一下PoolSubpage的初始化的代码:
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;
// >>>10表示除以2^10,也就是除以2^4,再除以2^6
// 这里为什么是16,64两个数字呢,elemSize是经过normCapacity处理的数字,最小值为16;
// 所以一个page最多可能被分成pageSize/16段内存,而一个long可以表示64个bit的状态;
// 因此最多需要pageSize/16/64个元素就能保证所有段的状态都可以管理
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
init(head, elemSize);
}
由于normCapacity的容量是经过处理过的,最小为16,所以一个page最多可以被分成pageSize/16个段,而一个long型的数字占64bit,所以bitmap最大只需要8个long型的数字就可以把所有的bit都标记出来了。
PoolSubpage初始化完成了会调用init方法对bitmap进行初始化操作。但是init方法除了PoolSubpage初始化时调用外,当一个PoolSubpage被回收后重新进行分配时也会调用。让我们看一下init方法中做了哪些操作:
void init(PoolSubpage head, int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
// >>>6 表示除以2^6 也就是:maxNumElems/64,
// 一个long占64个bit,所以得出需要bitmapLength个long
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}
// 将page中划分的段都初始化为0,表示还未被分配掉
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
// 将该subpage加入到subpagePool中,下一次使用时可以直接从pool中获取subpage
addToPool(head);
}
对每一个段都初始化完成之后,就需要调用allocate方法进行段的分配了,具体的分配方法如下:
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
// 当前没有可用的element或者当前page已经被销毁了,则直接返回
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
// 查找当前page中下一个可分配的内存段的index
final int bitmapIdx = getNextAvail();
// 得到该element段在bitmap数组中的索引下标q
int q = bitmapIdx >>> 6;
// 将>=64的那一部分二进制抹掉得到一个小于64的数
int r = bitmapIdx & 63;
// 该步表示bitmap[q]==0
assert (bitmap[q] >>> r & 1) == 0;
// 把第bitmap[q]标记为1,表示该element段已经被分配出去了
bitmap[q] |= 1L << r;
// 如果当前page分配完element之后没有其他可用的段了则从arena的pool中移除
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}
- 首先判断当前page是否可用,如果当前page中没有可用的element了或者当前page已经被销毁了,那么直接返回
- 查找当前page中下一个可分配的内存段的index
- 紧接着得到该element段在bitmap数组中的索引下标q
- 把bitmap中下标为q的标记为1,表示该element段已经分配出去了
- 如果当前page在分配完element之后,没有其他可用的段了则将其从pool中移除
前面说了,当分配PoolSubpage时会优先从PoolThreadCache中去分配,当然刚开始的时候PoolThreadCache中是没有PoolSubpage的,当初始化好之后会把PoolSubpage加入到smallSubpagePool中去,具体的插入方法是将smallSubpagePool的head节点的next指向当前要加入的PoolSubpage。可以用下面简单的图形表示:
[pool head] [pool head]
| | ^
|next |next|
∨ |____|
[subpage]
那什么时候申请PoolSubpage能从PoolThreadCache中分配到内存呢?当ByteBuf使用完了释放的时候,调用PoolArena的free方法时,会通过PoolThreadCache的add方法把当前ByteBuf所属的chunk添加到一个用MemoryRegionCache包装的queue中去。下次再申请时首先到PoolThreadCache中去分配就可以了,那怎么保证线程安全的呢?原来add添加的线程和现在get获取的线程如果不是同一个怎么办呢?
其实PoolThreadCache是保存在一个叫PoolThreadLocalCache的FastThreadLocal类型的线程本地变量中的,每次获取或添加时总是操作的当前线程。
以上对PoolSubpage和PoolThreadCache类的分析也完成了,但是netty的内存管理中并不仅仅包括这几个类。除了对小于pageSize的内存可以通过加入线程中的缓存来优化外,对于大于pageSize的内存netty在内存分配竞技场PoolArena中也使用了几个PoolChunkList来进行管理,主要是根据每个chunk使用的频率进行区分,保存到不同的PoolChunkList中去。chunkList和chunk的关系可以用下面简化的图形来表示:
+------------------+ +-------+
| [c0] <--> [c1] | | chunk |
| chunkList | <---> | List |
+------------------+ +-------+
PoolArena中共定义了以下几个chunkList:
private final PoolChunkList q050;
private final PoolChunkList q025;
private final PoolChunkList q000;
private final PoolChunkList qInit;
private final PoolChunkList q075;
private final PoolChunkList q100;
每个chunkList有一对内存使用率的上下限指标:minUsage和maxUsage。
以上的chunkList保存的chunk的内存使用率如下所示:
- qInit:存储内存利用率0-25%的chunk
- q000:存储内存利用率1-50%的chunk
- q025:存储内存利用率25-75%的chunk
- q050:存储内存利用率50-100%的chunk
- q075:存储内存利用率75-100%的chunk
- q100:存储内存利用率100%的chunk
这些chunkList之间通过prev和next指针串成一个链,初始化PoolArena时同时初始化这些chunkList,并将它们之间的指向关系维护好了,具体代码如下:
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);
从初始化的代码可知:
q100的next为空,prev为q075
q075的next为q100,prev为q050
q050的next为q075,prev为q025
q025的next为q050,prev为q000
q000的next为q025,prev为空
qInit的next为q000,prev为qInit
具体可以通过下面这张图来表示:
[qInit]-->[q000]<-->[q025]<-->[q050]<-->[q075]<-->[q100]
一个chunk从生成到消亡的过程中,不会固定在某个chunkList中,随着内存的分配和释放,根据当前的内存使用率,他会在chunkList链表中前后移动。目的就是为了增加内存分配的成功率。