Netty内存池管理
PooledByteBufAllocator
是Netty
提供的内存池实现。它用于分配和管理ByteBuf
对象,减少频繁的内存分配和释放操作,提高性能。内存池将内存分为不同大小的块,每个大小对应一个内存池。内存池中的块被分为多个层级,包括Chunk
、Page
、Subpage
。
Chunk
是Netty
向操作系统申请内存的单位,所有的内存分配操作也是基于Chunk
完成的,Chunk
可以理解为Page
的集合,每个Chunk
默认大小为16M
。Page
是Chunk
用于管理内存的单位,Netty
中的Page
的大小为8K
,不要与Linux
中的内存页Page
混淆了。假如我们需要分配64K
的内存,需要在Chunk
中选取8个Page
进行分配。
Subpage
负责Page
内的内存分配,假如我们分配的内存大小远小于Page
,直接分配一个Page
会造成严重的内存浪费,所以需要将Page
划分为多个相同的子块进行分配,这里的子块就相当于Subpage
。按照Tiny
和Small
两种内存规格,SubPage
的大小也会分为两种情况。在Tiny
场景下,最小的划分单位为16B
,按16B
依次递增,16B
、32B
、48B
…… 496B
;在Small
场景下,总共可以划分为512B
、1024B
、2048B
、4096B
四种情况。Subpage
没有固定的大小,需要根据用户分配的缓冲区大小决定,例如分配1K
的内存时,Netty
会把一个Page
等分为8个1K
的Subpage
。
PoolArena
PoolArena
是PooledByteBufAllocator
内部用于实际管理内存块的组件。PoolArena
的数据结构包含两个PoolSubpage
数组和六个PoolChunkList
,两个PoolSubpage
数组分别存放Tiny
和Small
类型的内存块,六个PoolChunkList
分别存储不同利用率的Chunk
,构成一个双向循环链表。其中PoolSubpage
用于分配小于8K
的内存,PoolChunkList
用于分配大于8K
的内存。
PoolSubpage
也是按照Tiny
和Small
两种内存规格,设计了tinySubpagePools
和smallSubpagePools
两个数组,在Tiny
场景下,内存单位最小为16B
,按16B
依次递增,共32种情况,Small
场景下共分为512B
、1024B
、2048B
、4096B
四种情况,分别对应两个数组的长度大小,每种粒度的内存单位都由一个PoolSubpage
进行管理。假如我们分配20B
大小的内存空间,也会向上取整找到32B
的PoolSubpage
节点进行分配。
PoolChunkList
用于Chunk
场景下的内存分配,PoolArena
中初始化了六个PoolChunkList
,分别为qInit
、q000
、q025
、q050
、q075
、q100
,它们分别代表不同的内存使用率。
qInit
,内存使用率为0 ~ 25%
的Chunk
。q000
,内存使用率为1 ~ 50%
的Chunk
。q025
,内存使用率为25% ~ 75%
的Chunk
。q050
,内存使用率为50% ~ 100%
的Chunk
。q075
,内存使用率为75% ~ 100%
的Chunk
。q100
,内存使用率为100%
的Chunk
。
六种类型的PoolChunkList
除了qInit
,它们之间形成了双向链表。qInit
用于存储初始分配的PoolChunk
,因为在第一次内存分配时,PoolChunkList
中并没有可用的PoolChunk
,所以需要新创建一个PoolChunk
并添加到qInit
列表中。qInit
中的PoolChunk
即使内存被完全释放也不会被回收,避免PoolChunk
的重复初始化工作。其余类型的PoolChunkList
中的PoolChunk
当内存被完全释放后,PoolChunk
从链表中移除,对应分配的内存也会被回收。
在使用PoolChunkList
分配内存时,也就是分配大于8K
的内存,其链表的访问顺序是q050
->q025
->q000
->qInit
->q075
,遍历检查PoolChunkList
中是否有PoolChunk
可以用于内存分配。
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
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)) {
return;
}
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
boolean success = c.allocate(buf, reqCapacity, normCapacity);
assert success;
qInit.add(c);
}
这是一个折中的选择,在频繁分配内存的场景下,如果从q000
开始,会有大部分的PoolChunk
面临频繁的创建和销毁,造成内存分配的性能降低。如果从q050
开始,会使PoolChunk
的使用率范围保持在中间水平,降低了PoolChunk
被回收的概率,从而兼顾了性能。
PoolChunkList
PoolChunkList
负责管理多个PoolChunk
的生命周期,同一个PoolChunkList
中存放内存使用率相近的PoolChunk
,这些PoolChunk
同样以双向链表的形式连接在一起。因为PoolChunk
经常要从PoolChunkList
中删除,并且需要在不同的PoolChunkList
中移动,所以双向链表是管理PoolChunk
时间复杂度较低的数据结构。
每个PoolChunkList
都有内存使用率的上下限,minUsage
和maxUsage
,当PoolChunk
进行内存分配后,如果使用率超过maxUsage
,那么PoolChunk
会从当前PoolChunkList
移除,并移动到下一个PoolChunkList
。同理,PoolChunk
中的内存发生释放后,如果使用率小于minUsage
,那么PoolChunk
会从当前PoolChunkList
移除,并移动到前一个PoolChunkList
。
PoolChunk
Netty
内存的分配和回收都是基于PoolChunk
完成的,PoolChunk
是真正存储内存数据的地方,每个PoolChunk
的默认大小为16M
。
final class PoolChunk<T> implements PoolChunkMetric {
final PoolArena<T> arena;
final T memory; // 存储的数据
private final byte[] memoryMap; // 满二叉树中的节点是否被分配,数组大小为 4096
private final byte[] depthMap; // 满二叉树中的节点高度,数组大小为 4096
private final PoolSubpage<T>[] subpages; // PoolChunk 中管理的 2048 个 8K 内存块
private int freeBytes; // 剩余的内存大小
PoolChunkList<T> parent;
PoolChunk<T> prev;
PoolChunk<T> next;
// 省略其他代码
}
PoolChunk
可以理解为Page
的集合,Page
只是一种抽象的概念,实际在Netty
中Page
所指的是PoolChunk
所管理的子内存块,每个子内存块采用PoolSubpage
表示。Netty
会使用伙伴算法将PoolChunk
分配成2048个Page
,最终形成一颗满二叉树,二叉树中所有子节点的内存都属于其父节点管理。
PoolSubpage
它的主要作用是管理内存池中的小内存块,在分配的内存大小小于一个8K
时,会使用PoolSubpage
进行管理。
final class PoolSubpage<T> implements PoolSubpageMetric {
final PoolChunk<T> chunk;
private final int memoryMapIdx; // 对应满二叉树节点的下标
private final int runOffset; // PoolSubpage 在 PoolChunk 中 memory 的偏移量
private final long[] bitmap; // 记录每个小内存块的状态
// 与 PoolArena 中 tinySubpagePools 或 smallSubpagePools 中元素连接成双向链表
PoolSubpage<T> prev;
PoolSubpage<T> next;
int elemSize; // 每个小内存块的大小
private int maxNumElems; // 最多可以存放多少小内存块:8K/elemSize
private int numAvail; // 可用于分配的内存块个数
// 省略其他代码
}
PoolSubpage
通过管理小于PageSize
的内存块来优化内存分配。它从PoolChunk
中预留一块连续内存区域,并将其划分为多个小块。每个PoolSubpage
内部维护一个空闲块链表。当需要分配内存时,PoolSubpage
会检查其内存区域中的空闲块,并从中选择一个可用的块进行分配。这样通过细化管理和复用小块内存,PoolSubpage
能够高效地处理小块内存的分配请求,减少内存碎片化并提高性能。
PoolThreadCache
PoolThreadCache
是本地线程缓存,内存分配时,首先尝试从线程本地缓存PoolThreadCache
中获取内存块,如果缓存中没有,才会从全局内存池中获取。每个线程有自己的PoolThreadCache
,可以快速分配和回收内存块,减少对全局内存池的争用。PoolThreadCache
缓存Tiny
、Small
、Normal
三种类型的数据,而且根据堆内和堆外内存的类型进行了区分:
final class PoolThreadCache {
final PoolArena<byte[]> heapArena;
final PoolArena<ByteBuffer> directArena;
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
// 省略其他代码
}
PoolThreadCache
中有一个重要的数据结构,MemoryRegionCache
。MemoryRegionCache
有三个重要的属性,分别为queue
,sizeClass
和size
。MemoryRegionCache
实际就是一个队列,当内存释放时,将内存块加入队列当中,下次再分配同样规格的内存时,直接从队列中取出空闲的内存块。PoolThreadCache
将不同规格大小的内存都使用单独的MemoryRegionCache
维护。
内存分配流程
Netty
的内存分配流程首先从PoolThreadCache
中尝试分配内存。对于小于8K
的内存请求,Netty
首先检查本地线程缓存。如果缓存中没有足够的内存块,则会向PoolArena
请求。对于大于8K
的内存请求,Netty
直接向PoolArena
请求内存,不经过本地线程缓存。PoolArena
使用PoolChunk
来管理较大的内存块,通过伙伴算法和二叉树结构进行内存分配。对于小内存请求,PoolArena
使用PoolSubpage
来管理,按位图记录内存块的使用情况。释放的内存块会被缓存到PoolThreadCache
中,定期进行整理,并在线程退出时释放。
具体来说:
- 分配请求处理:当应用程序需要分配内存时,
Netty
首先检查是否可以从PoolThreadCache
中获得所需的内存块。PoolThreadCache
是Netty
为每个线程维护的缓存,它用于存储和管理小块内存。这种缓存机制目的是提高内存分配的速度,减少线程间的竞争。 - 线程缓存检查:如果
PoolThreadCache
中有足够的空闲内存块,Netty
将直接从缓存中分配内存。这种方式比从全局内存池中分配内存要快,因为它避免了对全局内存池的访问和管理开销。线程缓存的使用可以显著减少内存分配和释放的延迟。 - 请求
PoolArena
:当线程缓存中的内存不足以满足请求时,Netty
将向PoolArena
请求内存PoolArena
负责管理整个内存池中的内存块,它会根据内存块的大小选择适当的管理策略。 PoolArena
内存分配:PoolArena
处理内存分配时,会根据内存块的大小选择不同的管理策略。对于较小的内存块,PoolArena
使用PoolSubpage
进行内存分配。PoolSubpage
使用位图来跟踪和管理内存块的使用状态,来减少内存碎片。对于较大的内存块,PoolArena
使用PoolChunk
,PoolChunk
通过伙伴算法或其他内存管理策略来处理大块内存。PoolChunk
负责分配较大的内存区域,能够应对不同的内存请求。- 内存分配和回收:内存块从
PoolArena
的PoolSubpage
或PoolChunk
中分配后,会被返回给应用程序使用。释放内存时,内存块将被归还到相应的内存管理区域。根据内存块的大小和当前的缓存状态,内存块可能会被归还到线程缓存中,或者直接归还到PoolArena
中进行进一步管理。 - 线程缓存整理:为了保持线程缓存的有效性,Netty 定期对
PoolThreadCache
中的内存块进行整理。这一过程可以减少内存碎片,提高内存使用效率。当线程结束时,与该线程相关的线程缓存内存也会被释放和整理。