概述
-
Netty内部的内存管理机制。首先,Netty会预先申请一大块内存,在内存管理器中一般叫做Arena。
-
Netty的Arena由许多Chunk组成,而每个Chunk又由一个或多个Page组成。Chunk通过二叉树的形式组织Page,每个叶子节点表示一个Page,而中间节点表示内存区域,节点自己记录它在整个Arena中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。
PoolArena
1.内部结构
- 应用层的内存分配主要通过如下实现,但最终还是委托给PoolArena实现。
PooledByteBufAllocator.DEFAULT.directBuffer(128)
- poolArena的内部结构图解
- 所有内存分配的size都会经过normalizeCapacity进行处理,当size>=512时,size成倍增长512->1024->2048->4096->8192,而size<512则是从16开始,每次加16字节
2.分配内存的方式
-
PoolSubpage用于分配小于8k的内存;
-
tinySubpagePools:用于分配小于512字节的内存,默认长度为32,因为内存分配最小为16,每次增加16,直到512,区间[16,512)一共有32个不同值;
-
smallSubpagePools:用于分配大于等于512字节的内存,默认长度为4;
-
inySubpagePools和smallSubpagePools中的元素都是默认subpage。
-
poolChunkList用于分配大于8k的内存;
- qInit:存储内存利用率0-25%的chunk
- q000:存储内存利用率1-50%的chunk
- q025:存储内存利用率25-75%的chunk
- q050:存储内存利用率50-100%的chunk
- q075:存储内存利用率75-100%的chunk
- q100:存储内存利用率100%的chunk
3.poolArena实现内存分配的方法
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
final int normCapacity = normalizeCapacity(reqCapacity);
if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
int tableIdx;
PoolSubpage<T>[] table;
boolean tiny = isTiny(normCapacity);
if (tiny) { // < 512
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;
} else {
if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
tableIdx = smallIdx(normCapacity);
table = smallSubpagePools;
}
final PoolSubpage<T> head = table[tableIdx];
/**
* Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
* {@link PoolChunk#free(long)} may modify the doubly linked list as well.
*/
synchronized (head) {
final PoolSubpage<T> s = head.next;
if (s != head) {
assert s.doNotDestroy && s.elemSize == normCapacity;
long handle = s.allocate();
assert handle >= 0;
s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
if (tiny) {
allocationsTiny.increment();
} else {
allocationsSmall.increment();
}
return;
}
}
allocateNormal(buf, reqCapacity, normCapacity);
return;
}
if (normCapacity <= chunkSize) {
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
allocateNormal(buf, reqCapacity, normCapacity);
} else {
// Huge allocations are never served via the cache so just call allocateHuge
allocateHuge(buf, reqCapacity);
}
}
分析:
-
默认先尝试从poolThreadCache中分配内存,PoolThreadCache利用ThreadLocal的特性,消除了多线程竞争,提高内存分配效率;首次分配时,poolThreadCache中并没有可用内存进行分配,当上一次分配的内存使用完并释放时,会将其加入到poolThreadCache中,提供该线程下次申请时使用。
-
如果是分配小内存,则尝试从tinySubpagePools或smallSubpagePools中分配内存,如果没有合适subpage,则采用方法allocateNormal分配内存。
-
如果分配一个page以上的内存,直接采用方法allocateNormal分配内存。
-
第一次进行内存分配时,chunkList没有chunk可以分配内存,需通过方法newChunk新建一个chunk进行内存分配,并添加到qInit列表中。如果分配如512字节的小内存,除了创建chunk,还有创建subpage,
-
PoolSubpage在初始化之后,会添加到smallSubpagePools中,其实并不是直接插入到数组,而是添加到head的next节点。下次再有分配512字节的需求时,直接从smallSubpagePools获取对应的subpage进行分配。
PoolChunk
1.原理
为了能够简单的操作内存,必须保证每次分配到的内存时连续的。Netty中底层的内存分配和回收管理主要由PoolChunk实现,其内部维护一棵平衡二叉树memoryMap,所有子节点管理的内存也属于其父节点。
2.组成
-
poolChunk默认由2048个page组成,一个page默认大小为8k,图中节点的值为在数组memoryMap的下标。
- 如果需要分配大小8k的内存,则只需要在第11层,找到第一个可用节点即可。
- 如果需要分配大小16k的内存,则只需要在第10层,找到第一个可用节点即可。
- 如果节点1024存在一个已经被分配的子节点2048,则该节点不能被分配,如需要分配大小16k的内存,这个时候节点2048已被分配,节点2049未被分配,就不能直接分配节点1024,因为该节点目前只剩下8k内存。
-
poolChunk内部会保证每次分配内存大小为8K*(2n),为了分配一个大小为chunkSize/(2k)的节点,需要在深度为k的层从左开始匹配节点,那么如何快速的分配到指定内存?
3.分配内存
向PoolChunk申请一段内存
-
当需要分配的内存大于pageSize时,使用allocateRun实现内存分配。
-
否则使用方法allocateSubpage分配内存,在allocateSubpage实现中,会把一个page分割成多段,进行内存分配。
PoolSubpage
1.图解
-
之前介绍了如何在poolChunk中分配一块大于pageSize的内存,但在实际应用中,存在很多分配小内存的情况,如果也占用一个page,很浪费。
-
Netty提供了PoolSubpage把poolChunk的一个page节点8k内存划分成更小的内存段,通过对每个内存段的标记与清理标记进行内存的分配与释放
2.源码实现
- PoolSubpage方法源码
final class PoolSubpage<T> {
// 当前page在chunk中的id
private final int memoryMapIdx;
// 当前page在chunk.memory的偏移量
private final int runOffset;
// page大小
private final int pageSize;
//通过对每一个二进制位的标记来修改一段内存的占用状态
private final long[] bitmap;
PoolSubpage<T> prev;
PoolSubpage<T> next;
boolean doNotDestroy;
// 该page切分后每一段的大小
int elemSize;
// 该page包含的段数量
private int maxNumElems;
private int bitmapLength;
// 下一个可用的位置
private int nextAvail;
// 可用的段数量
private int numAvail;
...
}
- allocate方法源码
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
- allocateSubpage方法源码
private long allocateSubpage(int normCapacity) {
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
synchronized (head) {
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
int id = allocateNode(d);
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
freeBytes -= pageSize;
int subpageIdx = subpageIdx(id);
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) {
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
return subpage.allocate();
}
}
PoolChunkList
1.定义
PoolChunkList负责管理多个chunk的生命周期,在此基础上对内存分配进行进一步的优化。
2.源码实现
- PoolChunkList类源码
final class PoolChunkList<T> implements PoolChunkListMetric {
private final PoolChunkList<T> nextList;
private final int minUsage;
private final int maxUsage;
private PoolChunk<T> head;
private PoolChunkList<T> prevList;
...
}
分析:随着chunk中page的不断分配和释放,会导致很多碎片内存段,大大增加了之后分配一段连续内存的失败率,针对这种情况,可以把内存使用率较大的chunk放到PoolChunkList链表更后面。
- allocate方法源码
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
if (head == null) {
return false;
}
for (PoolChunk<T> cur = head;;) {
long handle = cur.allocate(normCapacity);
if (handle < 0) {
cur = cur.next;
if (cur == null) {
return false;
}
} else {
cur.initBuf(buf, handle, reqCapacity);
if (cur.usage() >= maxUsage) { // (1)
remove(cur);
nextList.add(cur);
}
return true;
}
}
}
3.小结
-
每个chunkList的都有一个上下限:minUsage和maxUsage,
-
两个相邻的chunkList,前一个的maxUsage和后一个的minUsage必须有一段交叉值进行缓冲,否则会出现某个chunk的usage处于临界值,而导致不停的在两个chunk间移动。
-
chunk的生命周期不会固定在某个chunkList中,随着内存的分配和释放,根据当前的内存使用率,在chunkList链表中前后移动。
本人才疏学浅,若有错,请指出,谢谢!
如果你有更好的建议,可以留言我们一起讨论,共同进步!
衷心的感谢您能耐心的读完本篇博文!