PoolChunk 深度解析
一、核心概念
PoolChunk 是 Netty 内存池化管理中的核心类,负责管理一块连续的内存区域。它使用完全平衡二叉树算法来高效地分配和释放内存。
1.1 基本术语
- Page(页): 可分配的最小内存单元
- Chunk(块): 由多个 Page 组成的内存块
- ChunkSize(块大小):
chunkSize = 2^maxOrder × pageSize - Run: 一个或多个连续的 Page
1.2 默认配置
// 默认值(在 PooledByteBufAllocator 中定义)
pageSize = 8192 (8KB)
maxOrder = 11
chunkSize = 2^11 × 8KB = 16MB
二、核心字段详解
2.1 内存相关字段
final T memory; // 实际的内存对象(byte[] 或 ByteBuffer)
final int offset; // 内存偏移量
final int chunkSize; // Chunk 总大小(默认 16MB)
final int pageSize; // Page 大小(默认 8KB)
final int maxOrder; // 二叉树最大深度(默认 11)
private int freeBytes; // 当前可用字节数
2.2 二叉树管理字段
private final byte[] memoryMap; // 内存分配状态数组
private final byte[] depthMap; // 节点深度数组
memoryMap 数组说明
memoryMap 是一个完全平衡二叉树的数组表示,用于跟踪内存分配状态:
- 数组大小:
2^(maxOrder+1)= 4096(当 maxOrder=11 时) - 索引 1: 根节点(代表整个 16MB)
- 索引 2,3: 第二层节点(各代表 8MB)
- 索引 2048-4095: 叶子节点(各代表 8KB 的 Page)
memoryMap 值的含义
// 初始化时:memoryMap[id] = depth_of_id
memoryMap[1] = 0 // 根节点,深度为 0
memoryMap[2] = 1 // 第二层,深度为 1
memoryMap[3] = 1
...
memoryMap[2048] = 11 // 叶子节点,深度为 11
// 分配后的状态:
// 1) memoryMap[id] = depth_of_id => 该节点空闲
// 2) memoryMap[id] > depth_of_id => 该节点部分被占用
// 3) memoryMap[id] = maxOrder + 1 => 该节点完全被占用(标记为 unusable)
2.3 Subpage 管理字段
private final PoolSubpage<T>[] subpages; // Subpage 数组
private final int subpageOverflowMask; // 用于判断是否 >= pageSize
private final int maxSubpageAllocs; // 最大 Subpage 数量(2^maxOrder)
2.4 链表结构字段
PoolChunkList<T> parent; // 所属的 ChunkList
PoolChunk<T> prev; // 前一个 Chunk
PoolChunk<T> next; // 后一个 Chunk
三、二叉树结构详解
3.1 树的层次结构
depth=0 1 个节点 (16MB) [索引 1]
depth=1 2 个节点 (8MB 每个) [索引 2-3]
depth=2 4 个节点 (4MB 每个) [索引 4-7]
depth=3 8 个节点 (2MB 每个) [索引 8-15]
...
depth=11 2048 个节点 (8KB 每个) [索引 2048-4095]
3.2 节点索引计算
// 父节点索引
parentId = id >>> 1 // 等价于 id / 2
// 左子节点索引
leftChildId = id << 1 // 等价于 id * 2
// 右子节点索引
rightChildId = (id << 1) + 1 // 等价于 id * 2 + 1
// 兄弟节点索引
siblingId = id ^ 1 // 异或运算,奇数变偶数,偶数变奇数
3.3 示例:分配 8KB 内存
初始状态(所有节点值为其深度):
[1:0]
/ \
[2:1] [3:1]
/ \ / \
[4:2][5:2][6:2][7:2]
...
分配 8KB 后(假设分配节点 2048):
1. 找到叶子节点 2048
2. 设置 memoryMap[2048] = 12 (unusable)
3. 更新父节点路径:
- memoryMap[1024] = min(12, 11) = 11
- memoryMap[512] = min(11, 10) = 10
- ... 一直到根节点
四、核心方法详解
4.1 allocate() - 内存分配入口
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
final long handle;
// 判断是分配 Run 还是 Subpage
if ((normCapacity & subpageOverflowMask) != 0) {
// normCapacity >= pageSize,分配 Run
handle = allocateRun(normCapacity);
} else {
// normCapacity < pageSize,分配 Subpage
handle = allocateSubpage(normCapacity);
}
if (handle < 0) {
return false; // 分配失败
}
// 初始化 ByteBuf
ByteBuffer nioBuffer = cachedNioBuffers != null ?
cachedNioBuffers.pollLast() : null;
initBuf(buf, nioBuffer, handle, reqCapacity);
return true;
}
关键点:
-
subpageOverflowMask:
~(pageSize - 1),用于快速判断大小- 当 pageSize = 8192 时,mask = 0xFFFFE000
normCapacity & mask != 0表示 >= 8KB
-
handle: 一个 long 值,编码了内存位置信息
- 低 32 位:memoryMap 索引
- 高 32 位:bitmap 索引(Subpage 使用)
4.2 allocateRun() - 分配连续页
private long allocateRun(int normCapacity) {
// 计算需要的深度
// 例如:分配 16KB,需要深度 10 的节点(16KB = 2^10 × 8KB)
int d = maxOrder - (log2(normCapacity) - pageShifts);
// 在深度 d 查找可用节点
int id = allocateNode(d);
if (id < 0) {
return id; // 没有可用节点
}
// 减少可用字节数
freeBytes -= runLength(id);
return id;
}
深度计算示例:
// 假设 pageSize = 8KB, maxOrder = 11, pageShifts = 13
// 分配 32KB:
log2(32KB) = 15
d = 11 - (15 - 13) = 9
// 需要深度 9 的节点(大小 = 2^(11-9) × 8KB = 32KB)
// 分配 8KB:
log2(8KB) = 13
d = 11 - (13 - 13) = 11
// 需要深度 11 的节点(叶子节点)
4.3 allocateNode() - 查找可用节点
private int allocateNode(int d) {
int id = 1; // 从根节点开始
int initial = -(1 << d); // 用于判断是否到达目标深度
byte val = value(id);
if (val > d) {
return -1; // 根节点都不满足,无法分配
}
// 向下遍历,直到找到深度为 d 的可用节点
while (val < d || (id & initial) == 0) {
id <<= 1; // 尝试左子节点
val = value(id);
if (val > d) {
id ^= 1; // 左子节点不可用,尝试右子节点
val = value(id);
}
}
// 标记为已使用
setValue(id, unusable);
updateParentsAlloc(id);
return id;
}
算法流程示例:
查找深度 2 的节点:
1. 从根节点 1 开始,val=0 < 2,继续
2. 移到左子节点 2,val=1 < 2,继续
3. 移到左子节点 4,val=2,且 id & initial == 1<<2,找到!
4. 标记节点 4 为 unusable
5. 更新父节点状态
4.4 allocateSubpage() - 分配小内存
private long allocateSubpage(int normCapacity) {
// 获取对应大小的 Subpage 池头节点
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
int d = maxOrder; // Subpage 只能从叶子节点分配
synchronized (head) {
// 分配一个 Page
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
subpage = new PoolSubpage<T>(head, this, id,
runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
// 重新初始化已存在的 Subpage
subpage.init(head, normCapacity);
}
return subpage.allocate();
}
}
Subpage 索引计算:
private int subpageIdx(int memoryMapIdx) {
// 移除最高位,得到偏移量
return memoryMapIdx ^ maxSubpageAllocs;
// 例如:memoryMapIdx = 2048, maxSubpageAllocs = 2048
// subpageIdx = 2048 ^ 2048 = 0
// memoryMapIdx = 2049
// subpageIdx = 2049 ^ 2048 = 1
}
4.5 updateParentsAlloc() - 更新父节点(分配时)
private void updateParentsAlloc(int id) {
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1); // 兄弟节点
// 父节点的值 = min(左子节点, 右子节点)
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
}
}
更新示例:
分配节点 8 后:
1. memoryMap[8] = 12 (unusable)
2. 更新父节点 4: min(memoryMap[8], memoryMap[9]) = min(12, 3) = 3
3. 更新父节点 2: min(memoryMap[4], memoryMap[5]) = min(3, 2) = 2
4. 更新父节点 1: min(memoryMap[2], memoryMap[3]) = min(2, 1) = 1
4.6 free() - 释放内存
void free(long handle, ByteBuffer nioBuffer) {
int memoryMapIdx = memoryMapIdx(handle);
int bitmapIdx = bitmapIdx(handle);
if (bitmapIdx != 0) {
// 释放 Subpage
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
synchronized (head) {
if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
return; // Subpage 还有其他分配,不释放整个 Page
}
}
}
// 释放 Run 或完全空闲的 Page
freeBytes += runLength(memoryMapIdx);
setValue(memoryMapIdx, depth(memoryMapIdx)); // 恢复为深度值
updateParentsFree(memoryMapIdx);
// 缓存 ByteBuffer
if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);
}
}
4.7 updateParentsFree() - 更新父节点(释放时)
private void updateParentsFree(int id) {
int logChild = depth(id) + 1;
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
logChild -= 1;
// 如果两个子节点都完全空闲,父节点也标记为完全空闲
if (val1 == logChild && val2 == logChild) {
setValue(parentId, (byte) (logChild - 1));
} else {
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
}
id = parentId;
}
}
五、关键辅助方法
5.1 位置计算方法
// 计算节点代表的内存大小
private int runLength(int id) {
return 1 << (log2ChunkSize - depth(id));
// 例如:depth=0, runLength = 1 << (24-0) = 16MB
// depth=11, runLength = 1 << (24-11) = 8KB
}
// 计算节点在 Chunk 中的偏移量
private int runOffset(int id) {
int shift = id ^ (1 << depth(id));
return shift * runLength(id);
// 例如:id=2048 (第一个叶子节点)
// shift = 2048 ^ 2048 = 0
// offset = 0 × 8KB = 0
}
5.2 Handle 编解码
// Handle 结构(64 位):
// [高 32 位: bitmapIdx] [低 32 位: memoryMapIdx]
private static int memoryMapIdx(long handle) {
return (int) handle; // 取低 32 位
}
private static int bitmapIdx(long handle) {
return (int) (handle >>> Integer.SIZE); // 取高 32 位
}
六、使用场景分析
6.1 分配 16KB 内存
// 1. 调用 allocate(buf, 16384, 16384)
// 2. 16384 >= 8192,调用 allocateRun(16384)
// 3. 计算深度:d = 11 - (14 - 13) = 10
// 4. 在深度 10 查找节点(大小 16KB)
// 5. 找到节点,标记为 unusable
// 6. 更新父节点状态
// 7. freeBytes -= 16384
6.2 分配 512 字节内存
// 1. 调用 allocate(buf, 512, 512)
// 2. 512 < 8192,调用 allocateSubpage(512)
// 3. 在深度 11 分配一个 Page(8KB)
// 4. 创建或初始化 PoolSubpage
// 5. 在 Subpage 中分配 512 字节
// 6. freeBytes -= 8192(整个 Page)
七、性能优化设计
7.1 缓存优化
// ByteBuffer 缓存,减少 GC
private final Deque<ByteBuffer> cachedNioBuffers;
// 使用两个数组分离关注点
private final byte[] memoryMap; // 分配状态
private final byte[] depthMap; // 节点深度(不变)
7.2 位运算优化
// 使用位运算代替除法和取模
parentId = id >>> 1; // 代替 id / 2
leftChildId = id << 1; // 代替 id * 2
siblingId = id ^ 1; // 快速获取兄弟节点
7.3 快速判断
// 使用掩码快速判断大小
if ((normCapacity & subpageOverflowMask) != 0) {
// >= pageSize
}
八、总结
8.1 核心特点
- 完全平衡二叉树: 高效的内存分配和释放
- 伙伴系统: 支持内存合并,减少碎片
- 分层管理: Run(大内存)和 Subpage(小内存)
- 位运算优化: 快速计算和判断
8.2 时间复杂度
- 分配: O(log n),n 为树的深度(maxOrder)
- 释放: O(log n)
- 查找: O(log n)
8.3 空间复杂度
- memoryMap: 2^(maxOrder+1) 字节 = 4KB
- depthMap: 2^(maxOrder+1) 字节 = 4KB
- subpages: 2^maxOrder 个引用 = 16KB(64 位系统)
- 总开销: 约 24KB(管理 16MB 内存)
8.4 适用场景
- 需要频繁分配和释放内存的场景
- 内存大小范围广泛(从几百字节到几 MB)
- 对性能要求高的网络应用
- 需要减少 GC 压力的场景
1334



被折叠的 条评论
为什么被折叠?



