# 精尽 Netty 源码解析 —— Buffer 之 Jemalloc(四)PoolChunkList # 1. 概述 在 [《精尽 Netty 源码解析 —— Buffer 之 Jemalloc(二)PoolChunk》](http://svip.iocoder.cn/Netty/ByteBuf-3-2-Jemalloc-chunk) ,我们看到 PoolChunk 有如下三个属性: ``` /** * 所属 PoolChunkList 对象 */ PoolChunkList parent; /** * 上一个 Chunk 对象 */ PoolChunk prev; /** * 下一个 Chunk 对象 */ PoolChunk next; ``` - 通过 `prev` 和 `next` 两个属性,形成一个**双向** Chunk 链表 `parent`( PoolChunkList )。 那么为什么需要有 PoolChunkList 这样一个链表呢?直接开始撸代码。 # 2. PoolChunkList `io.netty.buffer.PoolChunkList` ,实现 PoolChunkListMetric 接口,负责管理多个 Chunk 的生命周期,**在此基础上对内存分配进行进一步的优化**。 ## 2.1 构造方法 ``` /** * 所属 PoolArena 对象 */ private final PoolArena arena; /** * 下一个 PoolChunkList 对象 */ private final PoolChunkList nextList; /** * Chunk 最小内存使用率 */ private final int minUsage; /** * Chunk 最大内存使用率 */ private final int maxUsage; /** * 每个 Chunk 最大可分配的容量 * * @see #calculateMaxCapacity(int, int) 方法 */ private final int maxCapacity; /** * PoolChunk 头节点 */ private PoolChunk head; /** * 前一个 PoolChunkList 对象 */ // This is only update once when create the linked like list of PoolChunkList in PoolArena constructor. private PoolChunkList prevList; // TODO: Test if adding padding helps under contention //private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7; PoolChunkList(PoolArena arena, PoolChunkList nextList, int minUsage, int maxUsage, int chunkSize) { assert minUsage <= maxUsage; this.arena = arena; this.nextList = nextList; this.minUsage = minUsage; this.maxUsage = maxUsage; // 计算 maxUsage 属性 maxCapacity = calculateMaxCapacity(minUsage, chunkSize); } ``` - `arena` 属性,所属 PoolArena 对象。 - `prevList` + `nextList` 属性,上一个和下一个 PoolChunkList 对象。也就是说,PoolChunkList 除了**自身**有一条双向链表外,PoolChunkList 和 PoolChunkList **之间**也形成了一条双向链表。如下图所示: > FROM [《深入浅出Netty内存管理 PoolChunkList》](https://www.jianshu.com/p/a1debfe4ff02) > > [![双向链表](45-Netty 源码解析-Buffer 之 Jemalloc(四)PoolChunkList.assets/01.png)](http://static.iocoder.cn/images/Netty/2018_09_10/01.png)双向链表 - `head` 属性,PoolChunkList **自身**的双向链表的**头节点**。 - ``` minUsage ``` + ``` maxUsage ``` 属性,PoolChunkList 管理的 Chunk 们的内存使用率。 - 当 Chunk 分配的内存率超过 `maxUsage` 时,从当前 PoolChunkList 节点移除,添加到下一个 PoolChunkList 节点( `nextList` )。TODO 详细解析。 - 当 Chunk 分配的内存率小于 `minUsage` 时,从当前 PoolChunkList 节点移除,添加到上一个 PoolChunkList 节点( `prevList` )。TODO 详细解析。 - `maxCapacity` 属性,每个 Chunk 最大可分配的容量。通过 `#calculateMaxCapacity(int minUsage, int chunkSize)` 方法,来计算。代码如下: ``` /** * Calculates the maximum capacity of a buffer that will ever be possible to allocate out of the {@link PoolChunk}s * that belong to the {@link PoolChunkList} with the given {@code minUsage} and {@code maxUsage} settings. */ private static int calculateMaxCapacity(int minUsage, int chunkSize) { // 计算 minUsage 值 minUsage = minUsage0(minUsage); if (minUsage == 100) { // If the minUsage is 100 we can not allocate anything out of this list. return 0; } // Calculate the maximum amount of bytes that can be allocated from a PoolChunk in this PoolChunkList. // // As an example: // - If a PoolChunkList has minUsage == 25 we are allowed to allocate at most 75% of the chunkSize because // this is the maximum amount available in any PoolChunk in this PoolChunkList. return (int) (chunkSize * (100L - minUsage) / 100L); } // 保证最小 >= 1 private static int minUsage0(int value) { return max(1, value); } ``` - 为什么使用 `(int) (chunkSize * (100L - minUsage) / 100L)` 来计算呢?因为 Chunk 进入当前 PoolChunkList 节点,意味着 Chunk 内存已经分配了 `minUsage` 比率,所以 Chunk 剩余的容量是 `chunkSize * (100L - minUsage) / 100L` 。😈 是不是豁然开朗噢?! ## 2.2 allocate > 随着 Chunk 中 Page 的不断分配和释放,会导致很多碎片内存段,大大增加了之后分配一段连续内存的失败率。针对这种情况,可以把内存使用率较大的 Chunk 放到PoolChunkList 链表更后面。 `#allocate(PooledByteBuf buf, int reqCapacity, int normCapacity)` 方法,给 PooledByteBuf 对象分配内存块,并返回是否分配内存块成功。代码如下: ``` 1: boolean allocate(PooledByteBuf buf, int reqCapacity, int normCapacity) { 2: // 双向链表中无 Chunk 3: // 申请分配的内存超过 ChunkList 的每个 Chunk 最大可分配的容量 4: if (head == null || normCapacity > maxCapacity) { 5: // Either this PoolChunkList is empty or the requested capacity is larger then the capacity which can 6: // be handled by the PoolChunks that are contained in this PoolChunkList. 7: return false; 8: } 9: 10: // 遍历双向链表。注意,遍历的是 ChunkList 的内部双向链表。 11: for (PoolChunk cur = head;;) { 12: // 分配内存块 13: long handle = cur.allocate(normCapacity); 14: // 分配失败 15: if (handle < 0) { 16: // 进入下一节点 17: cur = cur.next; 18: // 若下一个节点不存在,返回 false ,结束循环 19: if (cur == null) { 20: return false; // 分配失败 21: } 22: // 分配成功 23: } else { 24: // 初始化内存块到 PooledByteBuf 对象中 25: cur.initBuf(buf, handle, reqCapacity); 26: // 超过当前 ChunkList 管理的 Chunk 的内存使用率上限 27: if (cur.usage() >= maxUsage) { 28: // 从当前 ChunkList 节点移除 29: remove(cur); 30: // 添加到下一个 ChunkList 节点 31: nextList.add(cur); 32: } 33: return true; // 分配成功 34: } 35: } 36: } ``` - 第 2 至 8 行:双向链表中无 Chunk,或者申请分配的内存超过 ChunkList 的每个 Chunk 最大可分配的容量,返回 `false` ,分配失败。 - 第 11 行:遍历双向链表。**注意,遍历的是 ChunkList 的内部双向链表**。 - 第 13 行:调用 `PoolChunk#allocate(normCapacity)` 方法,分配内存块。这块,可以结合 [《精尽 Netty 源码解析 —— Buffer 之 Jemalloc(二)PoolChunk》「2.2 allocate」](http://svip.iocoder.cn/Netty/ByteBuf-3-2-Jemalloc-chunk) 在复习下。 - 第 15 至 17 行:分配失败,进入下一个节点。 - 第 18 至 21 行:若下一个节点不存在,返回 `false` ,分配失败。 - 第 22 至 25 行:分配成功,调用 ``` PooledByteBuf##initBuf(PooledByteBuf buf, long handle, int reqCapacity) ``` 方法,初始化分配的内存块到 PooledByteBuf 中。这块,可以结合 《精尽 Netty 源码解析 —— Buffer 之 Jemalloc(二)PoolChunk》「2.5 initBuf」 在复习下。 - 第 26 至 32 行:超过当前 ChunkList 管理的 Chunk 的内存使用率上限,从当前 ChunkList 节点移除,并添加到“ 下 ”一个 ChunkList 节点。 - 第 29 行:调用 `#remove(PoolChunk cur)` 方法,解析见 [「2.4.2 remove」](https://svip.iocoder.cn/Netty/ByteBuf-3-4-Jemalloc-chunkList/#) 。 - 第 31 行:调用 `#remove(PoolChunk cur)` 方法,解析见 [「2.4.1 add」](https://svip.iocoder.cn/Netty/ByteBuf-3-4-Jemalloc-chunkList/#) 。 - 第 33 行:返回 `true` ,分配成功。 ## 2.3 free `#free(PoolChunk chunk, long handle)` 方法,释放 PoolChunk 的指定位置( `handle` )的内存块。代码如下: ``` 1: boolean free(PoolChunk chunk, long handle) { 2: // 释放 PoolChunk 的指定位置( handle )的内存块 3: chunk.free(handle); 4: // 小于当前 ChunkList 管理的 Chunk 的内存使用率下限 5: if (chunk.usage() < minUsage) { 6: // 从当前 ChunkList 节点移除 7: remove(chunk); 8: // 添加到上一个 ChunkList 节点 9: // Move the PoolChunk down the PoolChunkList linked-list. 10: return move0(chunk); 11: } 12: // 释放成功 13: return true; 14: } ``` - 第 3 行:调用 `PoolChunk#free(long handle)` 方法,释放指定位置的内存块。这块,可以结合 [《精尽 Netty 源码解析 —— Buffer 之 Jemalloc(二)PoolChunk》「2.3 free」](http://svip.iocoder.cn/Netty/ByteBuf-3-2-Jemalloc-chunk) 在复习下。 - 第 5 行:小于当前 ChunkList 管理的 Chunk 的内存使用率下限: - 第 7 行:调用 `#remove(PoolChunk cur)` 方法,从当前 ChunkList 节点移除。 - 第 10 行:调用 `#move(PoolChunk chunk)` 方法, 添加到“上”一个 ChunkList 节点。详细解析,见 [「2.4.3 move」](https://svip.iocoder.cn/Netty/ByteBuf-3-4-Jemalloc-chunkList/#) 。 - 第 13 行:返回 `true` ,释放成功。 ## 2.4 双向链表操作 ### 2.4.1 add `#add(PoolChunk chunk)` 方法,将 PoolChunk 添加到 ChunkList 节点中。代码如下: ``` 1: void add(PoolChunk chunk) { 2: // 超过当前 ChunkList 管理的 Chunk 的内存使用率上限,继续递归到下一个 ChunkList 节点进行添加。 3: if (chunk.usage() >= maxUsage) { 4: nextList.add(chunk); 5: return; 6: } 7: // 执行真正的添加 8: add0(chunk); 9: } ``` - 第 2 至 6 行:超过当前 ChunkList 管理的 Chunk 的内存使用率上限,调用 `nextList` 的 `#add(PoolChunk chunk)` 方法,继续递归到下一个 ChunkList 节点进行添加。 - 第 8 行:调用 `#add0(PoolChunk chunk)` 方法,执行真正的添加。代码如下: ``` /** * Adds the {@link PoolChunk} to this {@link PoolChunkList}. */ void add0(PoolChunk chunk) { chunk.parent = this; // <1> 无头节点,自己成为头节点 if (head == null) { head = chunk; chunk.prev = null; chunk.next = null; // <2> 有头节点,自己成为头节点,原头节点成为自己的下一个节点 } else { chunk.prev = null; chunk.next = head; head.prev = chunk; head = chunk; } } ``` - `<1>` 处,比较好理解,胖友自己看。 - `<2>` 处,因为 `chunk` **新**进入下一个 ChunkList 节点,一般来说,内存使用率相对较低,分配内存块成功率相对较高,所以变成新的首节点。 ### 2.4.2 remove `#remove(PoolChunk chunk)` 方法,从当前 ChunkList 节点移除。代码如下: ``` private void remove(PoolChunk cur) { // 当前节点为首节点,将下一个节点设置为头节点 if (cur == head) { head = cur.next; if (head != null) { head.prev = null; } // 当前节点非首节点,将节点的上一个节点指向节点的下一个节点 } else { PoolChunk next = cur.next; cur.prev.next = next; if (next != null) { next.prev = cur.prev; } } } ``` - 代码比较简单,胖友自己研究。 ### 2.4.3 move `#move(PoolChunk chunk)` 方法, 添加到“上”一个 ChunkList 节点。代码如下: ``` /** * Moves the {@link PoolChunk} down the {@link PoolChunkList} linked-list so it will end up in the right * {@link PoolChunkList} that has the correct minUsage / maxUsage in respect to {@link PoolChunk#usage()}. */ 1: private boolean move(PoolChunk chunk) { 2: assert chunk.usage() < maxUsage; 3: 4: // 小于当前 ChunkList 管理的 Chunk 的内存使用率下限,继续递归到上一个 ChunkList 节点进行添加。 5: if (chunk.usage() < minUsage) { 6: // Move the PoolChunk down the PoolChunkList linked-list. 7: return move0(chunk); 8: } 9: 10: // 执行真正的添加 11: // PoolChunk fits into this PoolChunkList, adding it here. 12: add0(chunk); 13: return true; 14: } ``` - 第 4 至 8 行:小于当前 ChunkList 管理的 Chunk 的内存使用率下限,调用 `#move0(PoolChunk chunk)` 方法,继续递归到上一个 ChunkList 节点进行添加。代码如下: ``` private boolean move(PoolChunk chunk) { assert chunk.usage() < maxUsage; // 小于当前 ChunkList 管理的 Chunk 的内存使用率下限,继续递归到上一个 ChunkList 节点进行添加。 if (chunk.usage() < minUsage) { // Move the PoolChunk down the PoolChunkList linked-list. return move0(chunk); } // 执行真正的添加 // PoolChunk fits into this PoolChunkList, adding it here. add0(chunk); return true; } ``` - 第 12 行:调用 `#add0(PoolChunk chunk)` 方法,执行真正的添加。 - 第 13 行:返回 `true` ,移动成功。 ## 2.5 iterator `#iterator()` 方法,创建 Iterator 对象。代码如下: ``` private static final Iterator EMPTY_METRICS = Collections.emptyList().iterator(); @Override public Iterator iterator() { synchronized (arena) { // 空,返回 EMPTY_METRICS if (head == null) { return EMPTY_METRICS; } // 生成数组,后生成 Iterator List metrics = new ArrayList(); for (PoolChunk cur = head;;) { metrics.add(cur); cur = cur.next; if (cur == null) { break; } } return metrics.iterator(); } } ``` ## 2.6 destroy `#destroy()` 方法,销毁。代码如下: ``` void destroy(PoolArena arena) { // 循环,销毁 ChunkList 管理的所有 Chunk PoolChunk chunk = head; while (chunk != null) { arena.destroyChunk(chunk); chunk = chunk.next; } // 置空 head = null; } ``` ## 2.7 PoolChunkListMetric `io.netty.buffer.PoolChunkListMetric` ,继承 Iterable 接口,PoolChunkList Metric 接口。代码如下: ``` public interface PoolChunkListMetric extends Iterable { /** * Return the minimum usage of the chunk list before which chunks are promoted to the previous list. */ int minUsage(); /** * Return the maximum usage of the chunk list after which chunks are promoted to the next list. */ int maxUsage(); } ``` ------ PoolChunkList 对 PoolChunkMetric 接口的实现,代码如下: ``` @Override public int minUsage() { return minUsage0(minUsage); } @Override public int maxUsage() { return min(maxUsage, 100); } ``` # 3. PoolChunkList 初始化 在 PoolChunkArena 中,初始化 PoolChunkList 代码如下: ``` // PoolChunkList 之间的双向链表 private final PoolChunkList q050; private final PoolChunkList q025; private final PoolChunkList q000; private final PoolChunkList qInit; private final PoolChunkList q075; private final PoolChunkList q100; /** * PoolChunkListMetric 数组 */ private final List chunkListMetrics; 1: protected PoolArena(PooledByteBufAllocator parent, int pageSize, 2: int maxOrder, int pageShifts, int chunkSize, int cacheAlignment) { 3: 4: // ... 省略其它无关代码 5: 6: // PoolChunkList 之间的双向链表,初始化 7: 8: q100 = new PoolChunkList(this, null, 100, Integer.MAX_VALUE, chunkSize); 9: q075 = new PoolChunkList(this, q100, 75, 100, chunkSize); 10: q050 = new PoolChunkList(this, q075, 50, 100, chunkSize); 11: q025 = new PoolChunkList(this, q050, 25, 75, chunkSize); 12: q000 = new PoolChunkList(this, q025, 1, 50, chunkSize); 13: qInit = new PoolChunkList(this, q000, Integer.MIN_VALUE, 25, chunkSize); 14: 15: q100.prevList(q075); 16: q075.prevList(q050); 17: q050.prevList(q025); 18: q025.prevList(q000); 19: q000.prevList(null); // 无前置节点 20: qInit.prevList(qInit); // 前置节点为自己 21: 22: // 创建 PoolChunkListMetric 数组 23: List metrics = new ArrayList(6); 24: metrics.add(qInit); 25: metrics.add(q000); 26: metrics.add(q025); 27: metrics.add(q050); 28: metrics.add(q075); 29: metrics.add(q100); 30: chunkListMetrics = Collections.unmodifiableList(metrics); 31: } ``` - PoolChunkList 之间的双向链表有 `qInit`、`q000`、`q025`、`q050`、`q075`、`q100` 有 6 个节点,在【第 6 至 20 行】的代码,进行**初始化**。链表如下: ``` // 正向 qInit -> q000 -> q025 -> q050 -> q075 -> q100 -> null // 逆向 null <- q000 <- q025 <- q050 <- q075 <- q100 qInit <- qInit ``` - 比较神奇的是, ``` qInit ``` 指向自己?! ``` qInit ``` 用途是,新创建的 Chunk 内存块 ``` chunk_new ``` ( 这只是个代号,方便描述 ) ,添加到 ``` qInit ``` 后,不会被释放掉。 - 为什么不会被释放掉?`qInit.minUsage = Integer.MIN_VALUE` ,所以在 `PoolChunkList#move(PoolChunk chunk)` 方法中,`chunk_new` 的内存使用率最小值为 0 ,所以肯定不会被释放。 - 那岂不是 `chunk_new` 无法被释放?随着 `chunk_new` 逐渐分配内存,内存使用率到达 25 ( `qInit.maxUsage` )后,会移动到 `q000` 。再随着 `chunk_new` 逐渐释放内存,内存使用率降到 0 (`q000.minUsage`) 后,就可以被释放。 - 当然,如果新创建的 Chunk 内存块 `chunk_new` **第一次**分配的内存使用率超过 25 ( `qInit.maxUsage` ),不会进入 `qInit` 中,而是进入后面的 PoolChunkList 节点。 - `chunkListMetrics` 属性,PoolChunkListMetric 数组。在【第 22 至 30 行】的代码,进行**初始化**。 # 666. 彩蛋 PoolChunList 相比 PoolSubpage 来说,又又又更加简单啦。 老艿艿整理了下 Arena、ChunkList、Chunk、Page、Subpage 的“操纵”关系如下图: [![PoolSubpage](45-Netty 源码解析-Buffer 之 Jemalloc(四)PoolChunkList.assets/02.png)](http://static.iocoder.cn/images/Netty/2018_09_10/02.png)PoolSubpage - 当然,这不是一幅严谨的图,仅仅表达“操纵”的关系。 参考如下文章: - Hypercube [《自顶向下深入分析Netty(十)–PoolChunkList》](https://www.jianshu.com/p/2b8375df2d1a) - 占小狼 [《深入浅出Netty内存管理 PoolChunkList》](https://www.jianshu.com/p/4856bd30dd56)