Netty内存池核心:PoolChunk深度解析
目录
Arena 和 Chunk 的关系是什么?
关键数据结构分析
PoolChunk 核心成员
内存管理相关结构
统计与配置信息
run 是什么意思?
runsAvailMap 的作用是什么?
allocate 函数入口
路径1:小内存分配 (Small Allocation)
路径2:标准内存分配 (Normal Allocation)
allocateRun(int runSize)
allocateSubpage
总结
PoolChunk.initBuf
PoolChunk.initBufWithSubpage
PooledByteBuf.init
内存分配与管理机制解析
free
子页(Subpage)内存释放
运行块(Run)内存释放与合并
Run合并算法 collapseRuns
ollapsePast:合并前驱空闲块
collapseNext:合并后继空闲块
内存合并数据结构操作
调用链路总结
特性
PoolChunk
是Netty内存池中最核心的内存管理单元,它将一大块连续内存(默认16MB)组织成可分配的小块。
核心设计思想
- 分层管理: 将大块内存按页(Page)为单位进行管理
- 位图索引: 使用long型handle编码内存位置信息
- 双重分配: 支持大块分配(run)和小块分配(subpage)
Arena
和 Chunk
的关系是什么?
我们可以用一个比喻来理解:
-
PoolChunk
: 就像一个 仓库。它实实在在地持有一大块内存(比如 16MB),是物理内存的拥有者。所有内存的“切割”(无论是切成run
还是subpage
)都发生在这个仓库内部。chunk.subpages
数组是这个仓库自己的 “台账”,记录着“第几号货架(runOffset
)被改造成了用于存放小件物品的格子架(PoolSubpage
)”。 -
PoolArena
: 就像一个 总调度中心。它不直接持有内存,但它管理着旗下所有的仓库(PoolChunk
)。arena.smallSubpagePools
是调度中心的 “可用资源看板”。这个看板上,按照小件物品的规格(sizeIdx
)分门别类,挂着所有仓库里 “尚未装满的格子架(PoolSubpage
)” 的快捷入口。
Chunk
是内存的 物理持有者。Arena
是Subpage
的 逻辑管理者和调度者。chunk.subpages
是Chunk
的私有台账,用于根据地址快速找到Subpage
对象。arena.smallSubpagePools
是Arena
的全局可用Subpage
链表,用于快速分配。
一个 PoolSubpage
对象,它物理上属于一个 Chunk
,但逻辑上被 Arena
所管理和调度。
subpage
本身就是一种特殊的 run
(通常只占1个page),所以更准确的说法是,一个 Chunk
内部由不同状态的 run
组成。
下面是一个更详细的示意图:
+--------------------------------------------------------------------------+
| PoolChunk (16MB) |
| |
| +--------------------------------------------------+ |
| | Allocated Run | <-- 已分配的普通内存块 (例如 64KB)
| | (由 8 个 page 组成) |
| +--------------------------------------------------+ |
| |
| +--------------------------------------------------+ |
| | Free Run | <-- 空闲的内存块,等待分配
| | (由 4 个 page 组成) |
| +--------------------------------------------------+ |
| |
| +--------------------------------------------------+ <-- 这是一个 run (1个page, 8KB), 但被特殊管理
| | Subpage Run | |
| | (由 PoolSubpage 对象管理, 内部用 bitmap 划分) | |
| | /--------------------------------------------\ | |
| | | ele| ele| ele| ele| ele| ... | ele| ele|free| | |
| | \--------------------------------------------/ | |
| +--------------------------------------------------+ |
| |
| +--------------------------------------------------+ |
| | Free Run | |
| | (由 N 个 page 组成) | |
| +--------------------------------------------------+ |
| |
| ... |
| |
+--------------------------------------------------------------------------+^|+-------------------------------------------------------------------+|
+------------------------------------------------------+ +---------------------------------+
| PoolArena (总调度中心) | | PoolSubpage 对象 |
| | | (一个 Java 对象, 不是物理内存) |
| smallSubpagePools[sizeIdx]: | |---------------------------------|
| +------+ +----------------------------------+ | | - chunk: 指向上面的 PoolChunk |
| | head | <--> | 指向 Subpage Run 的快捷入口(指针) | <----| - runOffset: 记录在 Chunk 中的位置 |
| +------+ +----------------------------------+ | | - bitmap: 管理内部小块的分配状态 |
| | | - ... |
+------------------------------------------------------+ +---------------------------------+
这个图展示了:
PoolChunk
是一整块连续的物理内存。- 内部被划分为不同状态的
run
(已分配、空闲、被当做Subpage
使用)。 - 一个
Subpage Run
物理上仍在Chunk
里,但它由一个单独的PoolSubpage
对象来管理。 PoolArena
通过它的smallSubpagePools
链表,持有对这个PoolSubpage
对象的引用(快捷入口),从而实现了跨Chunk
的高效调度。
关键数据结构分析
Handle编码机制
// handle的位布局:
// oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
// o: runOffset (页偏移), 15bit
// s: size (页数), 15bit
// u: isUsed (是否使用), 1bit
// e: isSubpage (是否子页), 1bit
// b: bitmapIdx (子页位图索引), 32bitprivate static final int SIZE_BIT_LENGTH = 15;private static final int INUSED_BIT_LENGTH = 1;private static final int SUBPAGE_BIT_LENGTH = 1;private static final int BITMAP_IDX_BIT_LENGTH = 32;
static final int IS_SUBPAGE_SHIFT = BITMAP_IDX_BIT_LENGTH; // 32
static final int IS_USED_SHIFT = SUBPAGE_BIT_LENGTH + IS_SUBPAGE_SHIFT; // 33
static final int SIZE_SHIFT = INUSED_BIT_LENGTH + IS_USED_SHIFT; // 34
static final int RUN_OFFSET_SHIFT = SIZE_BIT_LENGTH + SIZE_SHIFT; // 49
正如代码注释中 handle
的位布局所示: oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
-
private static final int BITMAP_IDX_BIT_LENGTH = 32;
- 含义:用于
subpage
(子页)中位图索引(bitmap index)的位数,占 32 位。 - 作用:当分配的是一个小内存块(subpage allocation)时,这 32 位用来定位这块内存在
PoolSubpage
的位图中的具体位置。如果不是 subpage 分配,这部分为 0。
- 含义:用于
-
private static final int SUBPAGE_BIT_LENGTH = 1;
- 含义:标记是否为
subpage
分配的位数,占 1 位。 - 作用:这是一个布尔标记位。如果为
1
,表示这次分配是subpage
级别的小内存;如果为0
,表示是run
级别的内存(一个或多个 page)。
- 含义:标记是否为
-
private static final int INUSED_BIT_LENGTH = 1;
- 含义:标记是否“已被使用”的位数,占 1 位。
- 作用:这也是一个布尔标记位。
1
表示这块内存正在被使用,0
表示空闲。
-
private static final int SIZE_BIT_LENGTH = 15;
- 含义:表示分配的内存大小的位数,占 15 位。
- 作用:这个大小不是以字节为单位,而是以
page
的数量为单位。
-
剩下的 15 位(64 - 32 - 1 - 1 - 15 = 15)用于
runOffset
。
下面这些 SHIFT
常量是根据上面的位长度计算出的位移量,用于通过位运算快速地从 handle
中存取信息。
-
static final int IS_SUBPAGE_SHIFT = BITMAP_IDX_BIT_LENGTH;
- 含义:
isSubpage
标志位的起始偏移量,值为 32。
- 含义:
-
static final int IS_USED_SHIFT = SUBPAGE_BIT_LENGTH + IS_SUBPAGE_SHIFT;
- 含义:
isUsed
标志位的起始偏移量,值为 32 + 1 = 33。
- 含义:
-
static final int SIZE_SHIFT = INUSED_BIT_LENGTH + IS_USED_SHIFT;
- 含义:
size
(页面数量)的起始偏移量,值为 1 + 33 = 34。
- 含义:
-
static final int RUN_OFFSET_SHIFT = SIZE_BIT_LENGTH + SIZE_SHIFT;
- 含义:
runOffset
(run 在 chunk 内的页面偏移量)的起始偏移量,值为 15 + 34 = 49。
- 含义:
PoolChunk
核心成员
-
final PoolArena<T> arena;
- 含义:指向拥有这个
PoolChunk
的PoolArena
。PoolArena
是更高一级的内存池管理器,它管理着多个PoolChunk
。
- 含义:指向拥有这个
-
final CleanableDirectBuffer cleanable;
- 含义:当
PoolChunk
管理的是堆外内存(Direct Memory)时,这个对象用于在PoolChunk
被垃圾回收时自动清理底层的堆外内存,防止内存泄漏。
- 含义:当
-
final Object base;
- 含义:底层内存的基对象。对于堆内存,它通常是
byte[]
数组;对于堆外内存,它是ByteBuffer
对象。
- 含义:底层内存的基对象。对于堆内存,它通常是
-
final T memory;
- 含义:
PoolChunk
所管理的实际内存块。泛型T
可以是byte[]
(堆内存)或ByteBuffer
(堆外内存)。
- 含义:
-
final boolean unpooled;
- 含义:一个布尔值,标记这个
Chunk
是否是池化的。true
表示这是一个特殊的、非池化的Chunk
,通常用于分配超过chunkSize
的超大内存,用完即弃。false
表示是标准的、可复用的池化Chunk
。
- 含义:一个布尔值,标记这个
内存管理相关结构
-
private final LongLongHashMap runsAvailMap;
- 含义:一个特殊的
HashMap
,键和值都是long
。 - 作用:用于快速查找空闲的
run
。key
是run
的起始页偏移量(runOffset),value
是这个run
对应的handle
。它同时存储了一个run
的首页和尾页的偏移量,这样在释放内存时,可以快速检查前后是否有相邻的空闲run
并进行合并。
- 含义:一个特殊的
-
private final IntPriorityQueue[] runsAvail;
- 含义:一个
IntPriorityQueue
(整数优先队列)数组。 - 作用:用于管理所有可用的
run
。数组的索引根据run
的大小(包含的 page 数量)来决定。每个队列内部的run
(以handle
的高32位整数形式存储)按runOffset
排序。这样可以保证总是优先分配地址最低的run
,有助于减少内存碎片。
- 含义:一个
-
private final ReentrantLock runsAvailLock;
- 含义:一个可重入锁。
- 作用:在多线程环境下,对
runsAvailMap
和runsAvail
的访问需要加锁,以保证线程安全。
-
private final PoolSubpage<T>[] subpages;
- 含义:
PoolSubpage
对象数组。 - 作用:当一个
page
被用于小内存分配时,它会被包装成一个PoolSubpage
对象来管理其内部更细粒度的内存块。这个数组的索引是run
的起始页偏移量,值是对应的PoolSubpage
对象。
- 含义:
两个结构分析见:
Netty PoolChunk依赖的自定义数据结构:IntPriorityQueue和LongLongHashMap -CSDN博客
统计与配置信息
-
private final LongAdder pinnedBytes = new LongAdder();
- 含义:一个
LongAdder
,是AtomicLong
的高性能替代,用于并发计数。 - 作用:记录当前
Chunk
中已经被分配出去并且正在被ByteBuf
实例使用的总字节数。这个值对于内存使用率的监控非常重要。
- 含义:一个
-
final int pageSize;
- 含义:页大小,单位是字节。默认是 8192 (8KB)。是
PoolChunk
中内存管理的基本单位。
- 含义:页大小,单位是字节。默认是 8192 (8KB)。是
-
final int pageShifts;
- 含义:
pageSize
以 2 为底的对数,即log2(pageSize)
。例如,如果pageSize
是 8192 (2^13),pageShifts
就是 13。 - 作用:用于通过位移运算快速计算偏移量,比乘除法效率高。
- 含义:
-
final int chunkSize;
- 含义:整个
PoolChunk
的总大小,单位是字节。默认是 16MB。
- 含义:整个
-
final int maxPageIdx;
- 含义:
Chunk
内最大的页索引,等于(chunkSize / pageSize) - 1
。
- 含义:
-
private final Deque<ByteBuffer> cachedNioBuffers;
- 含义:一个双端队列,用于缓存
ByteBuffer
对象。 - 作用:当
PoolChunk
管理的是堆外内存时,分配出的PooledDirectByteBuf
需要一个ByteBuffer
对象作为视图来访问内存。为了避免每次都创建新的ByteBuffer
对象造成 GC 压力,这里会缓存一些用过的ByteBuffer
对象以供复用。
- 含义:一个双端队列,用于缓存
-
int freeBytes;
- 含义:当前
Chunk
中剩余的空闲字节数。
- 含义:当前
run
是什么意思?
在 PoolChunk
的设计中,run
是一个核心的内存管理概念。可以把它理解为 “一段连续的内存页(page)”。
在 PoolChunk.java
文件开头的注释中,有非常清晰的定义:
//...* Notation: The following terms are important to understand the code* > page - a page is the smallest unit of memory chunk that can be allocated* > run - a run is a collection of pages* > chunk - a chunk is a collection of runs* > in this code chunkSize = maxPages * pageSize
//...
- Page(页): 内存管理的最小单位,默认大小是 8KB。
- Chunk(块): 一个大的内存块,由多个
page
组成,默认大小是 16MB。 - Run(段): 介于
page
和chunk
之间。当需要分配一块不大不小(通常是大于一个page
)的内存时,PoolChunk
不会一页一页地分配,而是直接分配一个run
,这个run
包含了一个或多个连续的page
。
简单来说,run
就是 PoolChunk
中一次中等或大型内存分配的基本单位。
runOffset
指的是 一个 run
在其所属的 PoolChunk
中的起始位置,这个位置是以 page
为单位的偏移量。
我们再次看 PoolChunk.java
中关于 handle
的注释:
//...* handle:* -------* a handle is a long number, the bit layout of a run looks like:** oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb** o: runOffset (page offset in the chunk), 15bit* s: size (number of pages) of this run, 15bit* u: isUsed?, 1bit* e: isSubpage?, 1bit* b: bitmapIdx of subpage, zero if it's not subpage, 32bit
//...
从这里可以看出:
runOffset
是handle
(一个64位的long
值,用于唯一标识一次内存分配)的一部分。- 它被明确定义为 page offset in the chunk,即在
chunk
内的 页偏移量。 - 它占据了
handle
的高15位。
举个例子:
假设一个 PoolChunk
的 pageSize
是 8KB。
- 如果一个
run
的runOffset
是0
,意味着这个run
从这个chunk
的第0
个page
开始,也就是从字节偏移量0
开始。 - 如果一个
run
的runOffset
是5
,意味着这个run
从这个chunk
的第5
个page
开始,其在chunk
内的实际字节偏移量就是5 * pageSize = 5 * 8192 = 40960
。
总结一下:
run
: 一段连续的内存页,是PoolChunk
中进行中/大型内存分配的单位。runOffset
:run
在PoolChunk
中的起始页编号(索引)。
runsAvailMap
的作用是什么?
简单来说,runsAvailMap
的核心作用是 支持高效的内存块合并,以对抗内存碎片化。
在内存管理中,当一块内存被释放时,一个非常重要的优化是检查它前后的内存块是否也是空闲的。如果是,就应该将这些连续的空闲块合并成一个更大的空闲块。这样做可以有效减少小块内存碎片,提高内存的利用率。
runsAvailMap
就是实现这个“检查并合并”机制的关键数据结构。它是一个 HashMap
,存储了所有 空闲 run
的边界信息。
- Key:
run
的页偏移量 (runOffset
)。 - Value: 这个
run
对应的handle
。
它的巧妙之处在于:对于每一个空闲的 run
,它会同时存储两条记录:一条以“起始页”的偏移量为 Key,另一条以“结束页”的偏移量为 Key。两条记录的 Value 都是同一个 handle
。
runsAvailMap
主要在以下三个场景被使用:
-
插入 (当一个
run
变为空闲时)- 场景:
PoolChunk
初始化时、一个大run
被切分后剩余部分、一个run
被释放时。 - 方法:
insertAvailRun(int runOffset, int pages, long handle)
- 行为: 向
runsAvailMap
中添加两条记录(如果pages > 1
),分别对应这个空闲run
的起始页和结束页。【如果这个 run 只包含 1 页,那么它的起始页就是结束页。只有当页数大于 1 时,起始页和结束页才不相同。】
- 场景:
-
查找 (当需要合并内存时)
- 场景: 当一个
run
被释放,需要检查其前后是否有可以合并的空闲run
时。 - 方法:
getAvailRunByOffset(int runOffset)
- 行为: 在
free()
->collapseRuns()
->collapsePast()
/collapseNext()
方法中被调用。collapsePast(handle)
: 检查当前释放块B
前面 是否有空闲块A
。它会用B
的起始页偏移量减 1(即A
的结束页偏移量)作为 Key 去runsAvailMap
中查找。collapseNext(handle)
: 检查当前释放块B
后面 是否有空闲块C
。它会用B
的结束页偏移量加 1(即C
的起始页偏移量)作为 Key 去查找。
- 场景: 当一个
-
删除 (当一个空闲
run
被分配使用时)- 场景: 当一个空闲
run
被allocateRun
方法选中并分配出去时。 - 方法:
removeAvailRun(long handle)
->removeAvailRun0(long handle)
- 行为: 从
runsAvailMap
中删除这个run
对应的记录,因为它不再是空闲状态。
- 场景: 当一个空闲
allocate
函数入口
allocate(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache cache)
方法。这是一个非常核心的方法,它负责在单个 PoolChunk
内存块中为 ByteBuf
分配内存。
这个函数的整体逻辑是:根据请求的内存大小,决定是进行“小内存(Subpage)分配”还是“标准内存(Run)分配”,然后执行相应的分配策略,并最终用分配到的内存信息来初始化传入的 PooledByteBuf
对象。
下面我们来递归地分析它的执行流程和所有关键的子函数。
// ... existing code ...boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache cache) {final long handle;if (sizeIdx <= arena.sizeClass.smallMaxSizeIdx) {// 路径1: 小内存分配 (Small allocation)// ...} else {// 路径2: 标准内存分配 (Normal allocation)// ...}ByteBuffer nioBuffer = cachedNioBuffers != null? cachedNioBuffers.pollLast() : null;initBuf(buf, nioBuffer, handle, reqCapacity, cache);return true;}
// ... existing code ...
- 参数:
buf
: 一个待初始化的PooledByteBuf
对象。reqCapacity
: 用户请求的原始容量。sizeIdx
: 规格化后的容量索引。Netty 将所有请求大小归一化到预定义的规格中,sizeIdx
就是这个规格的 ID。cache
: 当前线程的PoolThreadCache
,用于线程本地缓存。
- 核心逻辑: 通过
sizeIdx
判断请求的是小内存还是标准内存。arena.sizeClass.smallMaxSizeIdx
是区分这两种内存的阈值索引。
路径1:小内存分配 (Small Allocation)
当请求的内存小于等于某个阈值时(默认是 pageSize
,即 8KB),会进入这个分支。小内存分配以 PoolSubpage
为单位进行管理。
// ... existing code ...if (sizeIdx <= arena.sizeClass.smallMaxSizeIdx) {final PoolSubpage<T> nextSub;// small// 从 Arena 的 subpage 池中获取一个可用的 subpagePoolSubpage<T> head = arena.smallSubpagePools[sizeIdx];head.lock();try {nextSub = head.next;if (nextSub != head) {// Case 1.1: 找到了一个可用的 subpagehandle = nextSub.allocate();assert handle >= 0;assert isSubpage(handle);nextSub.chunk.initBufWithSubpage(buf, null, handle, reqCapacity, cache);return true;}// Case 1.2: 没有可用的 subpage,需要新分配一个 run 来创建 subpagehandle = allocateSubpage(sizeIdx, head);if (handle < 0) {return false;}assert isSubpage(handle);} finally {head.unlock();}}
// ... existing code ...
Case 1.1: PoolArena
中有现成的 PoolSubpage
arena.smallSubpagePools[sizeIdx]
:PoolArena
中为每种规格的小内存维护了一个PoolSubpage
的双向链表。这里根据sizeIdx
找到对应规格的链表头head
。head.lock()
: 对链表头加锁,因为多个线程可能同时访问这个链表。nextSub = head.next
: 尝试从链表中获取第一个PoolSubpage
。如果nextSub != head
,说明链表不为空。nextSub.allocate()
: 这是PoolSubpage
的核心方法。它在一个page
(8KB)内部进行更细粒度的分配。- 内部实现:
PoolSubpage
内部用一个long
数组bitmap
来标记每个内存块的使用情况(0
代表可用,1
代表已用)。 allocate()
会调用findNextAvailable()
在bitmap
中找到第一个为0
的位,这个位的位置就是这次分配的内存块的索引。- 然后,它将该位置
1
,并根据这个索引和subpage
自身的信息生成一个handle
返回。这个handle
的低32位就是bitmap
索引。
- 内部实现:
initBufWithSubpage(...)
: 用上一步获取的handle
初始化ByteBuf
。- 它会从
handle
中解析出runOffset
(subpage 在 chunk 中的页偏移) 和bitmapIdx
(内存在 subpage 中的块索引)。 - 计算最终的物理偏移量:
offset = (runOffset << pageShifts) + bitmapIdx * s.elemSize
。 - 调用
buf.init(...)
完成初始化。
- 它会从
Case 1.2: 没有现成的 PoolSubpage
,需要创建新的
如果 nextSub == head
,说明 PoolArena
中没有可用的 PoolSubpage
了,此时需要先从当前 PoolChunk
中分配一个 run
,然后将这个 run
初始化为一个新的 PoolSubpage
。
allocateSubpage(sizeIdx, head)
:calculateRunSize(sizeIdx)
: 首先,计算创建一个能容纳这种规格(sizeIdx
)小内存的PoolSubpage
需要多大的run
。这个大小通常是一个pageSize
(8KB)。allocateRun(runSize)
: (递归调用) 调用下面的标准内存分配逻辑,从当前Chunk
中分配一个run
。这是整个流程的关键一步,我们稍后详细分析。如果分配失败,返回-1。new PoolSubpage<T>(...)
: 如果allocateRun
成功,就用分配到的run
的信息(runOffset
,runSize
)创建一个新的PoolSubpage
对象。subpages[runOffset] = subpage
: 将新创建的subpage
存入当前Chunk
的subpages
数组中,方便后续管理。subpage.allocate()
: 在这个全新的subpage
上调用allocate()
,分配第一个内存块并返回其handle
。
路径2:标准内存分配 (Normal Allocation)
当请求的内存大于小内存阈值时,直接分配一个或多个连续的 page
,即一个 run
。
// ... existing code ...} else {// normal// runSize 必须是 pageSize 的倍数int runSize = arena.sizeClass.sizeIdx2size(sizeIdx);handle = allocateRun(runSize);if (handle < 0) {return false;}assert !isSubpage(handle);}ByteBuffer nioBuffer = cachedNioBuffers != null? cachedNioBuffers.pollLast() : null;initBuf(buf, nioBuffer, handle, reqCapacity, cache);
// ... existing code ...
runSize = arena.sizeClass.sizeIdx2size(sizeIdx)
: 获取规格化后的内存大小。allocateRun(runSize)
: 这是标准分配的核心,也是小内存分配中创建Subpage
的基础。initBuf(...)
: 用allocateRun
返回的handle
初始化ByteBuf
。- 它从
handle
中解析出runOffset
。 - 计算物理偏移量:
offset = runOffset(handle) << pageShifts
。 - 计算最大长度:
maxLength = runSize(pageShifts, handle)
。 - 调用
buf.init(...)
完成初始化。
- 它从
allocateRun(int runSize)
此函数负责在 PoolChunk
中找到并分配一块指定大小、连续的 run
。
// ... existing code ...private long allocateRun(int runSize) {int pages = runSize >> pageShifts;int pageIdx = arena.sizeClass.pages2pageIdx(pages);runsAvailLock.lock();try {// 1. 寻找最合适的可用 runint queueIdx = runFirstBestFit(pageIdx);if (queueIdx == -1) {return -1;}// 2. 从优先队列中取出 runIntPriorityQueue queue = runsAvail[queueIdx];long handle = queue.poll();// ...// 3. 从 runsAvailMap 中移除removeAvailRun0(handle);// 4. 如果取出的 run 比需要的大,则进行切分handle = splitLargeRun(handle, pages);// 5. 更新空闲字节数并返回 handleint pinnedSize = runSize(pageShifts, handle);freeBytes -= pinnedSize;return handle;} finally {runsAvailLock.unlock();}}
// ... existing code ...
runFirstBestFit(pageIdx)
: 采用 "Best Fit" 策略寻找最合适的空闲run
。runsAvail
是一个IntPriorityQueue
数组,按run
的大小分桶。- 此方法从
pageIdx
(请求的大小)开始遍历runsAvail
数组,找到第一个不为空的队列。这确保了找到的run
是 "大于等于请求大小的最小run
",从而减少内存碎片。
queue.poll()
: 从找到的优先队列中取出一个run
。因为是优先队列,且按runOffset
排序,所以能保证取出的总是地址最低的run
。removeAvailRun0(handle)
: 将这个run
从runsAvailMap
中移除,因为它不再是空闲状态。splitLargeRun(handle, needPages)
: 这是内存管理的核心优化。- 如果找到的
run
(totalPages
)比需要的needPages
大,就把它切分成两部分。 - 第一部分,大小为
needPages
,被标记为“已使用”(inUsed=1
),然后作为本次分配的结果返回。 - 第二部分,即剩余的
remPages
,被构造成一个新的空闲run
的handle
,并通过insertAvailRun
方法重新放回空闲run
管理体系(runsAvail
和runsAvailMap
)中,以备将来使用。 - 如果大小正好,就直接标记为“已使用”并返回。
- 如果找到的
- 更新
freeBytes
并返回最终的handle
。
allocateSubpage
allocateSubpage
的核心逻辑就是先从当前 PoolChunk
中分配一个 run
,然后将这个 run
包装成一个 PoolSubpage
对象,用于进行后续更细粒度的内存分配。
所以,你可以理解为:PoolSubpage
是一个 run
的“特殊形态”,这个 run
被专门用来管理小内存的分配。
我们来逐行解析这个函数,看看它是如何工作的。
调用时机: 这个方法会在 allocate()
函数中被调用,但前提是:
- 用户请求的是一个小内存(
sizeIdx <= arena.sizeClass.smallMaxSizeIdx
)。 PoolArena
中对应规格的smallSubpagePools
链表为空,即没有现成的、未满的PoolSubpage
可供使用。
此时,系统就必须创建一个全新的 PoolSubpage
来满足分配需求。
// ... existing code ...private long allocateSubpage(int sizeIdx, PoolSubpage<T> head) {// 1. 分配一个新的 runint runSize = calculateRunSize(sizeIdx);// runSize 必须是 pageSize 的倍数long runHandle = allocateRun(runSize);if (runHandle < 0) {return -1;}// 2. 获取 run 的信息int runOffset = runOffset(runHandle);assert subpages[runOffset] == null;int elemSize = arena.sizeClass.sizeIdx2size(sizeIdx);// 3. 创建并初始化 PoolSubpagePoolSubpage<T> subpage = new PoolSubpage<>(head, this, pageShifts, runOffset,runSize(pageShifts, runHandle), elemSize);// 4. 在 Chunk 中记录这个新的 Subpagesubpages[runOffset] = subpage;// 5. 在新的 Subpage 上进行第一次分配return subpage.allocate();}
// ... existing code ...
步骤分解:
-
int runSize = calculateRunSize(sizeIdx);
- 这一步不是简单地分配一个
pageSize
(8KB) 的run
。它会计算一个最优的runSize
。这个大小需要是pageSize
的整数倍,并且能被要分配的元素大小elemSize
整除,以避免内存浪费。
- 这一步不是简单地分配一个
-
long runHandle = allocateRun(runSize);
- 这是回答你问题的关键。它直接调用了我们之前分析过的
allocateRun
方法。 allocateRun
会在runsAvail
中寻找一个大小最合适的空闲run
。- 如果找到的空闲
run
比runSize
大,allocateRun
内部的splitLargeRun
方法就会将其 分裂 成两块:一块返回给当前调用(大小为runSize
),另一块(剩余部分)重新放回空闲列表。 - 所以,
allocateSubpage
的内存来源,正是通过分配(和可能的 分裂)一个run
得到的。
- 这是回答你问题的关键。它直接调用了我们之前分析过的
-
new PoolSubpage<T>(...)
- 用上一步分配到的
run
的信息(runOffset
、runSize
)来创建一个新的PoolSubpage
对象。 - 这个
PoolSubpage
对象从此刻起就接管了这块run
的内存,并负责将其划分为N个更小的elemSize
块进行管理。 head
参数很重要,它会将这个新创建的subpage
添加到PoolArena
的smallSubpagePools
链表中,以便后续的分配可以直接复用它。
- 用上一步分配到的
-
subpages[runOffset] = subpage;
PoolChunk
自身也需要一个引用来找到它的subpage
。这个subpages
数组就起到了这个作用,它的索引就是run
的偏移量runOffset
。
-
return subpage.allocate();
PoolSubpage
已经创建好了,但别忘了调用allocateSubpage
的初衷是为了满足一次内存分配请求。- 所以最后一步,是在这个全新的
subpage
上调用allocate()
方法,分配出第一个小内存块,并将其handle
返回给最初的调用者。
总结
PoolChunk.allocate
的分配过程是一个精巧的多层级策略:
- 大小判断: 首先根据请求大小,决定走 小内存(Subpage) 还是 标准内存(Run) 的分配路径。
- 小内存路径:
- 优先从
PoolArena
的smallSubpagePools
缓存中获取一个现成的PoolSubpage
。 - 在
PoolSubpage
内部通过bitmap
快速分配一个槽位。 - 如果
Arena
中没有,则退回到当前Chunk
,调用 标准内存分配 逻辑 (allocateRun
) 分配一个page
,并将其包装成一个新的PoolSubpage
,再进行分配。
- 优先从
- 标准内存路径:
- 直接调用
allocateRun
。 allocateRun
使用 "Best Fit" 策略在runsAvail
队列中找到最合适的空闲run
。- 如果找到的
run
过大,则执行splitLargeRun
将其一分为二,一部分用于本次分配,另一部分(余料)重新放回空闲池,最大化内存利用率。
- 直接调用
- 初始化: 无论通过哪种路径,最终都会得到一个
handle
,它包含了内存块的所有元信息(偏移、大小等),并用它来初始化PooledByteBuf
,完成分配。
这个递归和分层的设计,使得 Netty 的内存池能够高效地处理各种大小的内存请求,同时通过 split
和 merge
(释放时)机制,最大限度地减少内存碎片。
PoolChunk.initBuf
整个 initBuf 的调用链是一个分层初始化的过程:
-
PoolChunk.allocate : 负责从内存池中分配一个 run 或 subpage ,并生成一个 handle 。
-
PoolChunk.initBuf / initBufWithSubpage : 负责解析 handle ,计算出内存块在 PoolChunk 中的具体位置和大小。
-
PooledByteBuf.init : 负责将计算出的物理内存信息( chunk 、 offset 、 length 等)与 ByteBuf 的逻辑视图关联起来,完成最终的初始化。
这个过程确保了 PooledByteBuf 能够正确地引用和操作由 PoolChunk 管理的底层内存。
这是初始化 PooledByteBuf 的入口函数。它的作用是根据 handle 的类型( run 或 subpage )来选择不同的初始化路径。
-
输入参数 : PooledByteBuf 实例、 ByteBuffer 、 handle (包含分配信息的长整型)、请求容量 reqCapacity 、 PoolThreadCache 。
-
逻辑 :
-
使用 isSubpage(handle) 检查 handle 是否代表一个 subpage 。
-
如果是 subpage : 调用 initBufWithSubpage 进行初始化。
-
如果不是 subpage (即是一个 run ) :
-
计算最大长度 maxLength = runSize(pageShifts, handle) 。
-
调用 buf.init(...) 来初始化 ByteBuf ,传入 run 的相关信息(如偏移量 runOffset(handle) << pageShifts )。
-
-
void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity,PoolThreadCache threadCache) {if (isSubpage(handle)) {initBufWithSubpage(buf, nioBuffer, handle, reqCapacity, threadCache);} else {int maxLength = runSize(pageShifts, handle);buf.init(this, nioBuffer, handle, runOffset(handle) << pageShifts,reqCapacity, maxLength, arena.parent.threadCache());}
}
PoolChunk.initBufWithSubpage
这个函数专门处理从 PoolSubpage 分配的内存的初始化。
// ... existing code ...void initBufWithSubpage(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity,PoolThreadCache threadCache) {int runOffset = runOffset(handle);int bitmapIdx = bitmapIdx(handle);PoolSubpage<T> s = subpages[runOffset];assert s.isDoNotDestroy();assert reqCapacity <= s.elemSize : reqCapacity + "<=" + s.elemSize;int offset = (runOffset << pageShifts) + bitmapIdx * s.elemSize;buf.init(this, nioBuffer, handle, offset, reqCapacity, s.elemSize, threadCache);}
// ... existing code ...
-
输入参数 : 与 initBuf 相同。
-
逻辑 :
-
从 handle 中解码出 runOffset 和 bitmapIdx 。
-
使用 runOffset 从 subpages 数组中找到对应的 PoolSubpage 实例。
-
计算内存的精确偏移量: offset = (runOffset << pageShifts) + bitmapIdx * s.elemSize 。
-
调用 buf.init(...) 来初始化 ByteBuf ,传入 subpage 的相关信息(如 maxLength 等于 s.elemSize )。
-
PooledByteBuf.init
这是最终执行初始化的地方,它设置 PooledByteBuf 的内部状态。它由 init0 私有方法实现。
// ... existing code ...void init(PoolChunk<T> chunk, ByteBuffer nioBuffer,long handle, int offset, int length, int maxLength, PoolThreadCache cache) {init0(chunk, nioBuffer, handle, offset, length, maxLength, cache);}// ... existing code ...private void init0(PoolChunk<T> chunk, ByteBuffer nioBuffer,long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
// ... existing code ...chunk.incrementPinnedMemory(maxLength);this.chunk = chunk;memory = chunk.memory;tmpNioBuf = nioBuffer;allocator = chunk.arena.parent;this.cache = cache;this.handle = handle;this.offset = offset;this.length = length;this.maxLength = maxLength;}
// ... existing code ...
-
输入参数 : PoolChunk 实例、 ByteBuffer 、 handle 、偏移量 offset 、长度 length 、最大长度 maxLength 、 PoolThreadCache 。
-
逻辑 :
-
增加 PoolChunk 中“固定”内存的引用计数 ( chunk.incrementPinnedMemory(maxLength) )。
-
设置 PooledByteBuf 的所有核心成员变量,包括 chunk , memory , handle , offset , length , maxLength , cache 等。
-
内存分配与管理机制解析
- 内存申请流程
调用链:
PoolArena.allocate() → PoolArena.newChunk()
内存分配方式:
-
直接内存 (DirectArena):
- 调用
newDirectChunk()
- 执行
PlatformDependent.allocateDirect(chunkSize)
或ByteBuffer.allocateDirect(chunkSize)
- 通过 JNI 调用底层系统调用 (如 malloc) 在堆外分配连续内存
- 最终包装成 ByteBuffer 对象传递给 PoolChunk 构造函数
- 调用
-
堆内存 (HeapArena):
- 调用
newHeapChunk()
- 执行
PlatformDependent.allocateUninitializedArray(chunkSize)
(等价于new byte[chunkSize]
) - 在 JVM 堆上分配字节数组传递给 PoolChunk
- 调用
关键点:
- 实际内存申请发生在 PoolArena 创建新 PoolChunk 时
- PoolChunk 是内存管理者而非申请者,负责切分和管理已申请的大块内存
memory 与 ByteBuf 的关联机制
关联要素:
- 对整个 PoolChunk 的引用
- 大内存块 memory 的引用
- 偏移量 (offset): 内存起始位置
- 长度 (length): 可用内存长度
操作原理:
读写操作示例: buf.setByte(index, value)执行过程:
1. 获取底层 memory 对象
2. 计算绝对地址: final_index = this.offset + index
3. 在 memory 的 final_index 位置进行读写
具体实现:
堆内存实现 (PooledHeapByteBuf):【实际过程使用了HeapByteBufUtil.setByte】
@Override
protected void _setByte(int index, int value) {// memory 为 byte[]((byte[]) memory)[idx(index)] = (byte) value; // idx(index) = offset + index
}
直接内存实现 (PooledDirectByteBuf):
@Override
protected void _setByte(int index, int value) {// memory 为 ByteBuffer((ByteBuffer) memory).put(idx(index), (byte) value);
}
设计优势
- 轻量级视图: PooledByteBuf 作为内存视图/指针,不包含实际数据
- 高效管理: 通过 (chunk → memory, offset, length) 精确标识内存区域
- 快速回收: 对象轻量便于快速回收重用
- 操作代理: 所有操作代理到指定内存区域,避免不必要的内存复制
这种设计实现了内存的高效管理和快速对象回收,是 Netty 高性能内存管理的关键机制。
free
此方法是 Netty 内存池回收机制的核心入口之一。当一个 ByteBuf
被释放时,其占用的内存最终会通过调用这个方法归还给它所属的 PoolChunk
。它的主要职责是:
- 接收一个代表内存块的句柄(
handle
) - 将其标记为可用
- 尝试与相邻的空闲内存块合并以减少碎片
- 最后更新
PoolChunk
的空闲状态
参数解析
参数 | 类型 | 说明 |
---|---|---|
long handle | 64位长整型 | 高度优化的数据结构,通过位运算编码了内存块的所有元信息(如是否是 subpage、在 PoolChunk 内的偏移量、大小等),是定位和释放内存的关键 |
int normCapacity | 整型 | 已标准化的容量。虽然在此方法内部没有直接使用,但通常由调用方(如 PoolArena )持有,用于决策(例如选择哪个 PoolSubpage 池) |
ByteBuffer nioBuffer | Java NIO Buffer | 当内存是直接内存(Direct Memory)时,这代表关联的 Java NIO ByteBuffer 对象。释放后可以被缓存以备复用,避免重复创建 ByteBuffer 对象的开销 |
代码逻辑分步详解
子页(Subpage)内存释放
void free(long handle, int normCapacity, ByteBuffer nioBuffer) {if (isSubpage(handle)) {int sIdx = runOffset(handle);PoolSubpage<T> subpage = subpages[sIdx];assert subpage != null;PoolSubpage<T> head = subpage.chunk.arena.smallSubpagePools[subpage.headIndex];// 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.head.lock();try {assert subpage.doNotDestroy;if (subpage.free(head, bitmapIdx(handle))) {//the subpage is still used, do not free itreturn;}assert !subpage.doNotDestroy;// Null out slot in the array as it was freed and we should not use it anymore.subpages[sIdx] = null;} finally {head.unlock();}}
//剩余释放run
关键步骤:
-
判断类型
调用isSubpage(handle)
检查是否属于 subpage(用于分配小块内存的页) -
获取 Subpage
- 通过
runOffset(handle)
计算 subpage 在subpages
数组中的索引sIdx
- 获取对应的
PoolSubpage
实例
- 通过
-
同步锁定
- 找到该 subpage 所在链表的头节点(
head
) - 加锁保证线程安全
- 找到该 subpage 所在链表的头节点(
-
释放子块
bitmapIdx(handle)
从句柄解码出内存块在 subpage 内部位图(bitmap)中的位置subpage.free()
更新位图,将该位置标记为可用
-
处理结果
- 如果
subpage.free
返回true
:说明 subpage 仍有其他内存块在使用,直接返回 - 如果返回
false
:说明 subpage 完全空闲,将subpages
数组对应槽位置为null
- 如果
运行块(Run)内存释放与合并
int runSize = runSize(pageShifts, handle);//start free runrunsAvailLock.lock();try {// collapse continuous runs, successfully collapsed runs// will be removed from runsAvail and runsAvailMaplong finalRun = collapseRuns(handle);//set run as not usedfinalRun &= ~(1L << IS_USED_SHIFT);//if it is a subpage, set it to runfinalRun &= ~(1L << IS_SUBPAGE_SHIFT);insertAvailRun(runOffset(finalRun), runPages(finalRun), finalRun);freeBytes += runSize;} finally {runsAvailLock.unlock();}
关键步骤:
-
适用场景
- 当释放的内存是普通 run
- 或 subpage 被完全清空后
-
同步锁定
使用runsAvailLock
保护对 run 可用性数据结构的并发访问 -
内存合并(Coalescing)
collapseRuns(handle)
为核心方法- 检查被释放的 run 前后是否有空闲 run,合并成更大的连续 run
-
更新句柄状态
finalRun &= ~(1L << IS_USED_SHIFT)
:清除"使用中"标志位finalRun &= ~(1L << IS_SUBPAGE_SHIFT)
:清除"是子页"标志位
-
重新加入可用列表
insertAvailRun()
将空闲 run 添加回可用数据结构(如runsAvailMap
)
-
更新空闲字节数
freeBytes += runSize
更新 PoolChunk 的空闲字节总数
缓存 NIO ByteBuffer
if (nioBuffer != null && cachedNioBuffers != null &&cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {cachedNioBuffers.offer(nioBuffer);}
}
优化点:
- 针对直接内存(Direct Buffer)的优化
- 如果传入
nioBuffer
且缓存未满,则存入cachedNioBuffers
队列 - 下次分配时可直接复用,避免调用
ByteBuffer.allocateDirect()
,降低系统调用和 GC 压力
总结
PoolChunk.free
方法的设计亮点:
-
高效回收机制:区分 subpage 和 run,适配不同大小内存块的回收需求
-
内存合并机制:通过
collapseRuns
有效对抗内存碎片化 -
对象复用优化 缓存
ByteBuffer
提升直接内存分配性能 -
线程安全:精细的锁策略保证多线程环境下的安全性
这是 Netty 高性能内存管理体系的基石,体现了空间与时间效率的完美平衡。
Run合并算法 collapseRuns
collapseRuns
是Netty内存池PoolChunk
中实现内存碎片合并的核心函数,其主要作用是在释放内存块时将相邻的空闲run(连续内存页)合并为更大的块,从而减少内存碎片并提高后续分配效率。以下是基于源代码的详细分析:
函数核心逻辑
PoolChunk.java
private long collapseRuns(long handle) {return collapseNext(collapsePast(handle));
}
该函数通过先合并前驱空闲块(collapsePast
)再合并后继空闲块(collapseNext
)的策略,实现连续内存的整合。合并过程会修改runsAvailMap
和runsAvail
两个核心数据结构:
- runsAvailMap:存储可用run的偏移量与句柄映射
- runsAvail:按页大小分类的可用run优先级队列
ollapsePast:合并前驱空闲块
private long collapsePast(long handle) {for (;;) {int runOffset = runOffset(handle);int runPages = runPages(handle);long pastRun = getAvailRunByOffset(runOffset - 1);if (pastRun == -1) return handle;int pastOffset = runOffset(pastRun);int pastPages = runPages(pastRun);if (pastOffset + pastPages == runOffset) {removeAvailRun(pastRun);handle = toRunHandle(pastOffset, pastPages + runPages, 0);} else {return handle;}}
}
- 逻辑:从当前run向前查找相邻空闲块,若存在连续空间则合并
- 核心调用:
getAvailRunByOffset
:通过runsAvailMap
查找前驱runremoveAvailRun
:从可用队列移除被合并的runtoRunHandle
:生成合并后的新run句柄
collapseNext:合并后继空闲块
private long collapseNext(long handle) {for (;;) {int runOffset = runOffset(handle);int runPages = runPages(handle);long nextRun = getAvailRunByOffset(runOffset + runPages);if (nextRun == -1) return handle;int nextOffset = runOffset(nextRun);int nextPages = runPages(nextRun);if (runOffset + runPages == nextOffset) {removeAvailRun(nextRun);handle = toRunHandle(runOffset, runPages + nextPages, 0);} else {return handle;}}
}private long getAvailRunByOffset(int runOffset) {return runsAvailMap.get(runOffset);}
- 逻辑:与
collapsePast
对称,向后查找并合并连续空闲块 - 终止条件:找不到后继空闲块或非连续空间时返回
内存合并数据结构操作
-
removeAvailRun0实现(实际执行从映射表删除):
private void removeAvailRun0(long handle) {int runOffset = runOffset(handle);int pages = runPages(handle);runsAvailMap.remove(runOffset);if (pages > 1) {runsAvailMap.remove(lastPage(runOffset, pages));} }
- 同时删除run的起始页和结束页映射(仅当页数>1时)
-
insertAvailRun实现(合并后新run的插入):
PoolChunk.javaprivate void insertAvailRun(int runOffset, int pages, long handle) {int pageIdxFloor = arena.sizeClass.pages2pageIdxFloor(pages);runsAvail[pageIdxFloor].offer((int) (handle >> BITMAP_IDX_BIT_LENGTH));insertAvailRun0(runOffset, handle);if (pages > 1) {insertAvailRun0(lastPage(runOffset, pages), handle);} }
- 将新run按页大小分类插入
runsAvail
队列 - 在
runsAvailMap
中注册起始页和结束页
- 将新run按页大小分类插入
内存管理中的作用
- 碎片抑制:通过合并相邻空闲块,维持大尺寸连续内存区域,提高大内存分配成功率
- 分配效率:配合
runFirstBestFit
分配策略,实现最佳适配 - 线程安全:通过
runsAvailLock
保证并发环境下的数据一致性
调用链路总结
free() → collapseRuns() → collapsePast() → [getAvailRunByOffset/removeAvailRun]↓collapseNext() → [getAvailRunByOffset/removeAvailRun]↓insertAvailRun() → [更新runsAvail/runsAvailMap]
通过这套机制,Netty实现了媲美jemalloc的高效内存管理,在高并发场景下显著降低GC压力并提升内存利用率。
特性
线程安全
-
使用
ReentrantLock runsAvailLock
保护run分配/释放 -
PoolSubpage有独立的锁机制
内存碎片管理
-
最佳适配: 优先使用最小满足需求的run
-
连续合并: 释放时自动合并相邻的空闲run
-
偏移排序: 相同大小的run按偏移量排序,减少碎片
性能优化
- 位操作: 使用位操作快速编码/解码handle信息
- 缓存机制:
cachedNioBuffers
缓存ByteBuffer减少GC【当调用 ByteBuf 的 internalNioBuffer(...) 或 nioBuffer(...) 等方法,需要将 ByteBuf 的数据以 ByteBuffer 的形式访问时, PoolChunk 就会利用 cachedNioBuffers 来提供 ByteBuffer 对象。】 - 分层索引: 多级索引结构提高查找效率
PoolChunk通过这种精巧的设计,实现了高效的内存分配和回收,最大化内存利用率并最小化碎片产生。