Netty 作为一款高性能的网络应用程序框架,拥有自己的内存分配。其思想源于 jemalloc github ,可以说是 jemalloc 的 Java 版本。
本章源码基于 Netty 4.1.44 版本,该版本是采用 jemalloc3.x 的算法思想,而 4.1.45 以后的版本则基于 jemalloc4.x 算法进行重构,两者差别还是挺大的。
jemalloc 算法
jemalloc
是基于 Slab
而来,比 Slab 更加复杂。Slab 提升小内存分配场景下的速度和效率,jemalloc 通过 Arena
和 Thread Cache
在多线程场景下也有出色的内存分配效率。Arena
是分而治之思想的体现,与其让一个人管理全部内存,到不如将任务派发给多个人,每个人独立管理,互不干涉(线程竞争)。Thread Cache
是 tcmalloc
的核心思想,jemalloc 也把它借鉴过来。每个线程有自己的内存管理器,分配在这个线程内完成,就不需要和其他线程竞争。相关文档
- Facebook Engineering post: This article was written in 2011 and corresponds to jemalloc 2.1.0.
- jemalloc(3) manual page: The manual page for the latest release fully describes the API and options supported by jemalloc, and includes a brief summary of its internals.
Netty 底层的内存分配是采用 jemalloc 算法思想。
内存规格
Netty 保留了对不同大小的内存采用不同的分配策略,具体规格如上图所示。在 Netty 中定义了 io.netty.buffer.PoolArena.SizeClass
枚举类,用于描述上图的内存规格类型,分别是 Tiny、Small 和 Normal。当 >16MB
时,归为Huge类型。
Netty 在每个区域内又定义了更细粒度的内存分配单位,分别是 Chunk、Page 和 Subpage。
// io.netty.buffer.PoolArena.SizeClass
enum SizeClass {
Tiny,
Small,
Normal
}
内存规格化
Netty 需要对用户申请的内存大小进行 规格化 处理,目的是方便后续计算和内存分配。比如用户申请的内存大小为 31B,如果不进行内存规格化,直接返回 31B 内存大小,那不就成 DMA 内存分配了么?
通过内存规格化,将 31B 规格化为 32B,将 15MB 规格化 16MB。当然,对于不同类型的内存策略不同。
从上图可以看出一些端倪:
- 对于 Huge 级别的内存大小,用户申请多少内存就返回多少内存(如有必要,需要内存对齐)。
- 对于 tiny 、small 、normal 级别的内存,以 512B 为分界线有:
- 当 >=512B时,返回最接近 2^n 且大于用户申请内存的大小的值。比如申请内存大小为 513B,则返回 1024B。
- 当 <512B 时,返回最接近 16 的倍数且大于用户申请内存的大小的值。比如申请内存大小为 17B,则返回 32B; 申请内存大小为 46B,返回 48B。
内存规格化核心源码在 io.netty.buffer.PoolArena
对象中,PoolArena 是 Netty 管理内存最重要的一个类:
// io.netty.buffer.PoolArena#normalizeCapacity
/**
* 对用户申请的内存大小作规格化处理,单位: 字节
*/
int normalizeCapacity(int reqCapacity) {
// reqCapacity < 0 ,抛出异常
checkPositiveOrZero(reqCapacity, "reqCapacity");
// #1 用户申请的大小超过 16M(16777216字节),则定义为大内存
if (reqCapacity >= chunkSize) {
// 若directMemoryCacheAlignment > 0,表明需要进行内存对齐
return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity);
}
// #2 对 tiny、small、normal 类型的内存规格化处理
// #2-1 对非tiny类型(16MB>capacity>= 512B)的内存大小规格化
if (!isTiny(reqCapacity)) {
// 通过位移让最高位1的其余低位都为1
// 比如reqCapacity = 0101(5),则规范化后的结果为 1000(8)
// 这段代码可以背下来,因为不管netty不是java的源码,都有应用
int normalizedCapacity = reqCapacity;
// 减一是让原本为2^n的值结果也是原值,如果不减一,则返回的是2^n+1
normalizedCapacity --;
normalizedCapacity |= normalizedCapacity >>> 1;
normalizedCapacity |= normalizedCapacity >>> 2;
normalizedCapacity |= normalizedCapacity >>> 4;
normalizedCapacity |= normalizedCapacity >>> 8;
normalizedCapacity |= normalizedCapacity >>> 16;
normalizedCapacity ++;
// 结果溢出,舍弃最高位
if (normalizedCapacity < 0) {
normalizedCapacity >>>= 1;
}
assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;
return normalizedCapacity;
}
// #2-2 对 <512 内存进行规格化(Tiny)
if (directMemoryCacheAlignment > 0) {
// 需要内存对齐
return alignCapacity(reqCapacity);
}
// 若申请容量恰好是16的倍数,则直接返回
if ((reqCapacity & 15) == 0) {
return reqCapacity;
}
// 找到距离 reqCapacity 最近的下一个16的倍数的值
// 比如申请容量为47B,则最近的且大于47B的数就是48B
return (reqCapacity & ~15) + 16;
}
// 可以设置 io.netty.allocator.directMemoryCacheAlignment 值
/**
* 当directMemoryCacheAlignment>0时,才需要进行内存对齐动作。
* 而netty的directMemoryCacheAlignment默认值为0,一般不需要进行内存对齐。除非有特殊要求。
* 这个内存对齐的代码也很有意思,比如当前内存需要和16对齐,容量为 18:
* 1.首先获得对齐掩码: 16-1=15
* 2.通过 & 操作获取 delta 值
* 3.如果余数为0,表明已经和16对齐,直接返回即可。如果非0,则申请容量值+对齐值-delta就是结果
* 18+16-3
* 其实上面这个步骤可以看成求余操作
* 比如申请容量为18,按16对齐,
* 1.求余: 18%16=2
* 2.求值: 18+16-2=32
*/
int alignCapacity(int reqCapacity) {
// &操作,相当于求余数
// directMemoryCacheAlignmentMask = directMemoryCacheAlignment - 1
int delta = reqCapacity & directMemoryCacheAlignmentMask;
// 如果为0,则说明 reqCapacity 已经对齐,直接返回
// 如果不为0,需要对齐
// 对齐的意思是,比如当前 directMemoryCacheAlignment = 16,则申请内存在范围1~16之间会返回 16
// 在17~32之间则会返回 32,内存以16字节进行对齐,总之,得到的结果总为 N*directMemoryCacheAlignment
return delta == 0 ? reqCapacity : reqCapacity + directMemoryCacheAlignment - delta;
}
获取最接近 2^n 的数
我们需要对申请的内存进行规格化,便于计算和管理。下面是将 1025 进行规格化的过程:
上面一连串的位移计算,看得眼花缭乱。其实最主要的目的是找到最接近 2n 且大于用户申请内存的大小的值。思路是把二进制 0100 0000 0001(1025)
变成 0111 1111 1111(2048)
。记初始值为 i
,原始值的二进制最高位为 1
的序号记为 j
,具体执行过程描述如下:
- 先执行
i-1
操作,目的是解决当值为 2^n 时也能得到本身,而非2^(n+1)。 再执行 i |= i>>>1 运算,目的是赋值第 j-1 位的值为 1。已经第 j 位位置确定为 1,那么无符号右移一位后第 j-1 也为 1。再与原值进行 | 运算后更新第 j-1 的值。此时,原值的第 j、j-1 都确定为 1,那么接下来就可以无符号右移两倍,让 j-2、j-3 赋值为 1。由于 int 类型有 32 位,所以只需要进行 5 次运算,每次分别无符号右移1、2、4、8、16 就可让小于 i 的所有位都赋值为 1。
获取最近的下一个16的倍数值
其实思路很简单,先把低四位的值抹去(变成0),再加上
16
就得到了目标值。(reqCapacity & ~15) + 16;
// 0000 0000 0000 0000 0000 0000 0010 1100 (44)(原始值)
// 0000 0000 0000 0000 0000 0000 0000 1111 (15)(15)
// 1111 1111 1111 1111 1111 1111 1111 0000 (-16)(~15) // ~15
// 0000 0000 0000 0000 0000 0000 0010 0000 (32)(reqCapacity& ~15) // 抹去低4位
// 0000 0000 0000 0000 0000 0000 0011 0000 (48) // +16,补值
小结
Netty 通过大量的位运算来提升性能,但代码的可读性不太好。因此,大家可以通过一边网上搜索一边通过模拟位运算体会各个位之间的变化过程。
- 位运算的使用技巧,可以看看 位运算简介及实用技巧,里面讲得十分详细。
- Netty 和内存规格化的位运算技巧展示了三个:
- 一是找到离分配内存最近且大于分配内存的 2^n 值。
- 二是找到离分配内存最近且大于分配内存的16 倍的值。
- 三是通过掩码判断是否大于某个数。
- 内存规格化的单位是字节(byte),而非字(bit)。
Netty 内存池分配整体思路
- 首先,Netty 会向 操作系统 申请一整块 连续内存,称为 chunk(数据块),除非申请 Huge 级别大小的内存,否则一般大小为 16MB,使用 io.netty.buffer.PoolChunk 对象包装。具体长这样子:
- Netty将chunk进一步拆分为多个page,每个 page 默认大小为 8KB,因此每个 chunk 包含 2048 个 page。为了对小内存进行精细化管理,减少内存碎片,提高内存使用率,Netty 对 page 进一步拆分若干 subpage,subpage 的大小是动态变化的,最小为 16Byte。
- 计算: 当请求内存分配时,将所需要内存大小进行内存规格化,获得合适的内存值。根据值确认准确的树的高度。
- 搜索: 在该分组大小的相应高度中从左至右搜寻空闲分组并进行分配。
- 标记: 分组被标记为全部已使用,且通过循环更新其父节点标记信息。父节点的标记值取两个子节点标记值的最小的一个。
当然,上面说的只是整体思路,一时看还云里雾里的。相信经过下面的讲述能帮助你拔云见日。
Huge 分配逻辑概述
大内存分配比其他类型的内存分配稍微简单一点,操作的内存单元是 PoolChunk,它的容量大小是用户申请的容量(可满足内存对齐要求)。Netty 对 Huge 对象的内存块采用非池化管理策略,在每次请求分配内存时单独创建特殊的非池化 PoolChunk 对象,当对象内存释放时整个 PoolChunk 内存也会被释放。
大内存的分配逻辑是在 io.netty.buffer.PoolArena#allocateHuge 完成。
Normal 分配逻辑
Normal 级别分配的大小范围是 [4097B, 16M) 。核心思想是将 PoolChunk 拆分成 2048 个 page ,这是 Normal 分配的最小单位。每个 page 等大(pageSize=8KB),并在逻辑上通过一棵满二叉树管理这些 page 对象。我们申请的内存本质是组合若干个 page 。
Normal 的分配核心逻辑是在 PoolChunk#allocateRun(int) 完成。
Small 分配逻辑
Small 级别分配的大小范围是 (496B, 4096B] 。核心是把一个 page 拆分若干个 Subpage,PoolSubpage 就是这些若干个 Subpage 的化身,有效解决小内存场景造成内存碎片的问题。
一个 page 大小为 8192B,有且只有四种大小: 512B、1024B、2048B 和 4096B,以 2 倍递增。当申请的内存大小在 496B~4096B 范围内时,就能确定这四种中的一种。
当进行内存分配时,先在树的最底层找到一个空闲的 page,拆分成若干个 subpage,并构造一个 PoolSubpage 进行管理。选择第一个 subpage 用于此次申请,标记为已使用,并将 PoolSubpage 放置在 PoolSubpage[] smallSubpagePools 数组所对应的链表中。等下次申请等大容量内存时就可从 PoolSubpage[] 直接分配从链表中分配内存。
Tiny 分配逻辑
Tiny 级别分配的大小范围是 (0B, 496B] 。分配逻辑与 Small 类似,先找到空闲的 Page 然后将其拆分若干个 Subpage 并构造一个 PoolSubpage 对它们进行管理。随后选择第一个 subpage 用于此次申请,并将对象 PoolSubpage 放置在 PoolSubpage[] tinySubpagePools 数组所对应的链表中。等待下次分配时使用。区别在于如何定义若干个? Tiny 给出的定义逻辑是获取最接近 16N 的且大于规格值的大小。比如申请内存大小为 31B,找到最接近的下一个 161 的倍数且大于 31 的值是 32,因此,就把 Page 拆分成 8192/32=256 个 subpage,这里的若干个就是根据规格值确定的,它是可变的值。
PoolArena
上面讲述了针对不同级别 Netty 是如何完成内存分配的。接下来,我们先对一些类进行认识,为后续源码解读打下基础。
PoolArena 是进行池化内存分配的核心类,采用固定数量的多个 Arena 进行内存分配,默认与 CPU 核心数量有关,它是线程共享的对象,每个线程只会绑定一个 PoolArena,线程和 PoolArena 是多对一的关系。当某个线程首次申请内存分配时,会通过轮询(Round-Robin)方式得到一个 Arena,在该线程的整个生命周期内只和这个 Arena 打交道,前面也说过,PoolArena 是分治思想的体现,在多线程场景下有出色的表现。PoolArena 提供 DirectArena 和 HeapArena 子类,这是因为底层容器类型不同所以需要子类区分。但核心逻辑是在 PoolArena 完成的。PoolArena 的数据结构大致(除去监测指标数据)可分为两大类: 存储 PoolChunk 的 6 个 PoolChunkList 和 存储 PoolSubpage 的 2 个数组。PoolArena 构造器初始化也做了很多重要的工作,包含串联 PoolChunkList 以及初始化 PoolSubpage[] 。
初始化 PoolChunkList
q000
、q025
、q050
、q075
、q100
表示最低内存使用率。如下图所示
任意 PoolChunkList 都有内存使用率的上下限: minUsag、maxUsage。如果使用率超过 maxUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到下一个PoolChunkList 。同理,如果使用率小于 minUsage,那么 PoolChunk 会从当前 PoolChunkList 移除,并移动到前一个PoolChunkList。
每个 PoolChunkList 的上下限都有交叉重叠的部分,因为 PoolChunk 需要在 PoolChunkList 不断移动,如果临界值恰好衔接的,则会导致 PoolChunk 在两个 PoolChunkList 不断移动,造成性能损耗。
PoolChunkList 适用于 Chunk 场景下的内存分配,PoolArena 初始化 6 个 PoolChunkList 并按上图首尾相连,形成双向链表,唯独 q000 这个 PoolChunkList 是没有前向节点,是因为当其余 PoolChunkList 没有合适的 PoolChunk 可以分配内存时,会创建一个新的 PoolChunk 放入 pInit 中,然后根据用户申请内存大小分配内存。而在 p000 中的 PoolChunk ,如果因为内存归还的原因,使用率下降到 0%,则不需要放入 pInit,直接执行销毁方法,将整个内存块的内存释放掉。这样,内存池中的内存就有生成/销毁等完成生命周期流程,避免了在没有使用情况下还占用内存。
初始化 PoolSubpage[]
PoolSubpage 是对某一个 page 的化身,由于 Page 还可以按 elemSize 拆分成若干个 subpage,在 PoolArena 使用 PoolSubpage[] 数组来存储 PoolSubpage 对象,经过 PoolArena 后如下图所示:
还记得这幅图么:
对于 Small 它拥有四种不同大小的规格,因此 smallSupbagePools 的数组长度为 4,smallSubpagePools[0] 表示 elemSize=512B 的 PoolSubpage 对象的链表,smallSubpagePols[1] 表示 elemSize=1024B 的 PoolSubpages 对象的链表。以此类推,tinySubpagePools 原理一样,只不过划分的粒度(步长)比较少,以 16 的倍数递增。因此,由于 Tiny 大小限制,总共可分为 32 类,因此 tinySubpagePools 数组长度为 32。数组下标所对应的 size 容量不一样,且每个数组都对应一组双向链表。这两个数组用来存储 PoolSubpage 对象且按 PoolSubpage#elemSize 确定索引的位置 index,最后将它们构造双向链表。
源码
子类实现
继承体系如下图所示:
- PoolArenaMetric: 定义与 PoolArena 相关监控接口。
- PoolArena: 抽象类。定义了主要的核心变量和部分内存分配逻辑。由于存储数据容器不同,创建和销毁逻辑也有所不一样。因此它有两个子类,分别是 DirectArena、HeapArena。
抽象类 PoolArena 有几个子类必须实现的接口:
// io.netty.buffer.PoolArena
/**
* 返回一个「池化的」「PoolChunk」对象。
* @param pageSize 「页」大小
* @param maxOrder 「满二叉树」最大高度
* @param pageShifts 页偏移量
* @param chunkSize PoolChunk大小,默认一般是「16MB」
* @return
*/
protected abstract PoolChunk<T> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize);
/**
* 返回一个「非池化」「PoolChunk」对象
* @param capacity 申请内存容量大小
* @return
*/
protected abstract PoolChunk<T> newUnpooledChunk(int capacity);
/**
* 返回一个「池化的」ByteBuf 对象,用来包装物理内存
* @param maxCapacity
* @return
*/
protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity);
/**
* 内存拷贝
* @param src 数据源
* @param srcOffset 数据源起始位置
* @param dst 目标对象
* @param length 拷贝长度
*/
protected abstract void memoryCopy(T src, int srcOffset, PooledByteBuf<T> dst, int length);
/**
* 释放「PoolChunk」对象
* @param chunk
*/
protected abstract void destroyChunk(PoolChunk<T> chunk);
这些抽象方法就是 DirectArena 和 HeapArena 实现类的区别,具体细节就不再描述了。
PoolChunkList
PoolChunkList 是一个双向链表,用来存储 PoolChunk 对象,它指向 PoolChunk 链表的头结点。
而对于 PoolChunkList 节点本身来说,它与其他 PoolChunkList 也构成一个双向链表。如上图所示。PoolChunkList 内部定义比较简单:
// io.netty.buffer.PoolChunkList
final class PoolChunkList<T> implements PoolChunkListMetric {
// 当内存元素为「null」时就返回这个空的迭代器,可以减少空判断
private static final Iterator<PoolChunkMetric> EMPTY_METRICS
= Collections.<PoolChunkMetric>emptyList().iterator();
// 「PoolChunkList」是由「PoolArena」所管理的,所以持有「管理者」的引用
private final PoolArena<T> arena;
// 「PoolChunkList」和其他的「PoolChunkList」也会构成双向链表的结构
// q000 和 qInit 例外
private final PoolChunkList<T> nextList;
private PoolChunkList<T> prevList;
// 会存在多个「PoolChunkList」链表,它们的区别就是使用率不同
// 最小使用率
private final int minUsage;
// 最高使用率
private final int maxUsage;
// 一个「PoolChunk」所对应最高可分配的内存大小,根据「minUsage」计算得到该值。
// 比如当minUsage=75,计算公式为: chunkSize*(100-minUsage)/100=16777216*(100-75)/100=4194304
// 表示在「q075」内部中每个「PoolChunk」的最大内存使用容量不得超过4194304
private final int maxCapacity;
// 「PoolChunkList」指向头结点即可,每个「PoolChunk」也是一个双向链表
private PoolChunk<T> head;
// 以下定义添加、释放等方法
/**
* 从「PoolChunkList」分配「PooledByteBuf」对象
*/
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
if (normCapacity > maxCapacity) {
// 无法返回,直接返回「false」
return false;
}
// 从头结点遍历「PoolChunkList」,如果能有找到一个合适的「PoolChunk」分配并返回「true」
// 否则返回「false」
for (PoolChunk<T> cur = head; cur != null; cur = cur.next) {
// 利用「PoolChunk」分配内存
if (cur.allocate(buf, reqCapacity, normCapacity)) {
// 当前「PoolChunk」的使用率超过「PoolChunkList」的 「maxUsage」
// 移除该节点到下一个节点中。表示使用率已经到达我这个「PoolChunkList」的临界值了,
// 已经容不下你
if (cur.usage() >= maxUsage) {
remove(cur);
nextList.add(cur);
}
return true;
}
}
return false;
}
/**
* 释放「PoolChunk」对象
*/
boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
// 使用「PoolChunk」释放内存
chunk.free(handle, nioBuffer);
// 判断当前「PoolChunk」的「minUsage」,是否还有资格留在当前的「PoolChunkList」
if (chunk.usage() < minUsage) {
remove(chunk);
// 添加到前向节点的「PoolChunkList」中
return move0(chunk);
}
return true;
}
}
PoolChunk
PoolChunk 是 Netty 对 jemalloc3.x 算法思想的描述,它是 Netty 内存分配的最核心的类。
文档翻译
概述描述
page 是 Chunk 可分配的最小内存单元,Chunk 是 page 的集合,Chunk 大小的计算公式为 chunkSize = 2^{maxOrder} pageSize。
首先,我们分配一个 size = chunkSize 的字节数组,当需要创建一个给定大小的 ByteBuf 时,我们搜索字节数组中的第一个位置,该位置有足够的空闲空间来容纳请求的大小,并返回一个 long 类型的句柄值来编码这个偏移量信息(这个内存段然后被标记为保留,所以它总是由一个 ByteBuf 使用,而不是多个)。
为了简单起见,所有用户申请内存的大小都按 PoolArena#normalizecapacity 方法法进行规格化处理。这确保了当我们请求大小 >= pageSize 的内存段时,规格化容量等于下一个最近的2的次幂。
为了获取请求大小可用的第一个偏移量,我们构造了一棵*满二叉树(Compelte balanced binary) 从而加快搜索速度。使用数组 memoryMap 存储这棵树的信息。这棵树看起来看是这样的(括号中的表示每个节点的大小)
- depth=0 1 node (chunkSize)
- depth=1 2 nodes (chunkSize/2)
- …
- depth=d 2^d nodes (chunkSize/2^d)
- …
- depth=maxOrder 2^maxOrder nodes (chunkSize/2^{maxOrder} = pageSize)
当 depth=maxOrder 时,叶子节点是由 page 组成。
搜索算法
用符号在 memoryMap 中编码满二叉树。
- memoryMap 类型是 byte[],用来记录树的分配情况。初始值为对应节点所在的树的深度。
- memoryMap[id] = depth_of_id => 空闲/完全未分配。
- memoryMap[id] > depth_of_id => 至少有一个子节点已经被分配了,但其他子节点仍然可分配。
memoryMap[id] = maxOrder + 1 => 当前节点已经完成分配了,即当前节点处于不可用状态。
allocateNode(d)
目标是在对应深度从左到右找到第一个空闲的可分配的节点。参数 d 表示 depth。
从头结点开始。(depth=0 或 id=1)
- 如果 memoryMap[1] > d 表示这个 Chunk 无可用分配内存。
- 如果左节点的值 <=h,我们可以从左子树进行分配,重复直到找到空闲节点。
-
allocateRun(size)
分配一组 page。参数 size 表示规格化后的内存大小。
计算 size 所对应的深度。公式 d = log_2(chunkSize/size)。
-
allocateSubpage(size)
创建/初始化一个 normcacity 大小的新 PoolSubpage。创建/初始化任意 PoolSubpage 都会添加到拥有这个 PoolChunk 的 PoolArena 的子页内存池中。
使用 allocateNode(maxOrder) 找到任意空闲的页子节点,返回一个 handle 变量。
使用 handle 构建 PoolSubpage 对象并添加到 PoolArena 的 subpagePool 内存池中。
源码
PoolChunk 源码相对比较复杂,首先需要把定义的变量理解清楚,为后续内存分配源码分析打下基础。
// io.netty.buffer.PoolChunk
final class PoolChunk<T> implements PoolChunkMetric {
// 31
private static final int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1;
//「PoolChunk」对象是由「PoolArena」创建,持有创建者引用
final PoolArena<T> arena;
// 它有两种存在形式,对于堆内存,T为 byte[],对于直接内存,T为ByteBuffer
final T memory;
// 当前的PoolChunk是否使用内存池的方式进行管理
// 如果分配「Huge」级别内存,则unpooled=true
final boolean unpooled;
// 偏移量,默认值: 0。如果需要内存对齐,那就是这个是内存偏移量
// 具体见 PoolArena#offsetCacheLine(ByteBuffer memory)函数计算
final int offset;
// 记录树的使用情况。长度由公式「maxSubpageAllocs << 1」计算可得
// 而 maxSubpageAllocs = 1 << maxOrder,maxOrder默认值为「11」,
// 但可通过io.netty.allocator.maxOrder更改。但一般不会这么做。
// 故mrmoryMap数组长度为 4096
private final byte[] memoryMap;
// 记录树的每个节点所对应的深度,长度为 memoryMap.length。
// 第一层深度为「0」,第二层深度为「1」以此类推
private final byte[] depthMap;
// 我们知道,poolsubpage是用于对「page」的拆分,这是用来保存对「page」拆分后的「subpage」信息。
// 想象一下,当一个「poolchunk」都被拆分了,因此它最大长度应该为叶子节点的数量,即 1<<maxOrder
private final PoolSubpage<T>[] subpages;
// 子页溢出掩码。默认值为-8192。就是通过「位运算」和「8192」相比较。
// 当「req&subpageOverflowMask!=0」,表示req(所需内存容量)>=8192,使用allocateRun(int)分配内存
// 当「req&subpageOverflowMask==0」,表示req(所需内存容量)<8192,使用allocateSubpae(int)分配内存
// 1111 1111 1111 1111 1110 0000 0000 0000 (-8192)
private final int subpageOverflowMask;
// 页大小,默认值: 8192(8KB)
private final int pageSize;
// 页节点所代表的偏移量,默认值 13,用于计算申请大小「normCapacity」在「树」的哪一层
// 计算公式: int d = maxOrder - (log2(normCapacity)- pageShifts);
private final int pageShifts;
// 满二叉树的,默认值: 11
private final int maxOrder;
// 整个PoolChunk申请的内存大小,默认值为16MB
private final int chunkSize;
// 将「ChunkSize」取2的对数,默认值: 24
private final int log2ChunkSize;
// 指定代表叶节点的PooolSubPage数组所需初始化的长度,由1<<maxOrder计算得到
// 默认值:2048
private final int maxSubpageAllocs;
// 当某节点无空闲内存节点时,对应的 memoryMap[id]=unusable
// 默认值: maxOrder+1=12。
private final byte unusable;
// 缓存从内存创建的「ByteBuffer」对象。目的是减少GC。
// 如果无池化,则cacheNioBuffers可能为null
private final Deque<ByteBuffer> cachedNioBuffers;
// 剩余可用的字节数
private int freeBytes;
//
PoolChunkList<T> parent;
// 当前节点的前置节点
PoolChunk<T> prev;
// 当前节点的后置节点
PoolChunk<T> next;
// ...
}
相关方法一览:
这是只是为了让大家留有印象,等到源码分析时可以来这里看看对应的变量和方法到底做了些什么事情。PoolSubpage
PoolSubpage 是 Small、Tiny 级别分配内存时所使用到的对象。一个 PoolSubpage 对象对应一个 page。因此,一个 PoolSubpage 管理的内存大小为 8KB。
相关变量解释如下:// io.netty.buffer.PoolSubpage
final class PoolSubpage<T> implements PoolSubpageMetric {
// 归属于哪个「PoolChunk」,因为「PoolSubpage」也是从「PoolChunk」分配得到的
final PoolChunk<T> chunk;
// 位于memoryMap[] 数组的下标索引
private final int memoryMapIdx;
// 当前PoolSubpage相对起始的叶子节点的偏移量
private final int runOffset;
// 默认值: 8KB
private final int pageSize;
// 记录当前「PoolSubpage」内存使用情况。1表示使用中,0表示空闲。长度固定为「8」。
// 8的由来: pageSize默认大小为16MB,而「Subpage」最小内存大小值为16B,所以16MB/16B=512。
// 而一个「long」只能存储64个「Subpage」使用情况,共需要512/64=8个「long」型值才能完整表示
// 「PoolSubpage」使用情况。
private final long[] bitmap;
// 「PoolSubpage」也是一个双向链表的结构。其实,可以把「PoolSubpage」看成是对一个「page」的表示。
// 「PoolChunk」看成是多个「Page」的表示。
// 在某些情况下会存在多个「page」被「PoolSubpage」表示。因此,它们会根据自身的「最小粒度值」
// 连成双向链表。
PoolSubpage<T> prev;
PoolSubpage<T> next;
// 是否已被销毁
boolean doNotDestroy;
// 表示当前「page」被拆分成「subpage」的最小内存大小的值。
int elemSize;
// 当前PoolSubpage所管理的内存块总数。由 pageSize/elemSize
private int maxNumElems;
// 当前「PoolSubpage」用到多少个bitmap
private int bitmapLength;
// 记录下一个可用的节点,初始值: 0
private int nextAvail;
// 剩余可用的内存块个数
private int numAvail;
/**
* 「PoolSubpage」构造器
* @param head 从「PoolArena」对象中获取「PoolSubpage」结点做头结点
* @param chunk 当前「PoolSubpage」属性的「PoolChunk」对象
* @param memoryMapIdx 所属的「Page」的节点值
* @param runOffset 对存储容器为「byte[]」有用,表示偏移量
* @param pageSize 页大小,默认值为: 8KB
* @param elemSize 元素个数
*/
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
init(head, elemSize);
}
// 初始化「PoolSubpage」
void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
// 初始化各类参数
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
// 根据元素个数确定所需要bitmap个数,即确认「bitmapLength」值
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
// 添加至双向链表中,供后续分配使用
addToPool(head);
}
private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
// ...
}
PoolSubpage 管理小内存也是十分有技巧,待后面做详细解读。
再讲池化内存分配
在 ByteBuf 这一章节中我们讲过 ByteBufAllocator 分配器体系。但那里是从整个分配器体系讲解,与池化分配器相关的 PooledByteBufAllocator 只是简单的描述了初始化流程。现在我们继续从这里当做切入点,理清各个类之间如何分配和管理的。
首先我们要知道 PooledByteBufAllocator 是线程安全的类,我们可以通过 PooledByteBufAllocator.DEFAULT 获得一个 io.netty.buffer.PooledByteBufAllocator 池化分配器,这也是 Netty 推荐的做法之一。我们也了解到,PooledByteBufAllocator 会初始两个重要的数组,分别是 heapArenas 和 directArenas,所有的与内存分配相关的操作都会委托给 heapArenas 或 directArenas 处理,数组长度一般是通过 2CPU_CORE 计算得到。这里体现 Netty(准确地说应该是 jemalloc 算法思想) 内存分配设计理念,通过增加多个 Arenas 减少内存竞争,提高在多线程环境下分配内存的速度以及效率。数组 arenas 是由上面我们讲过的 PoolArena 对象构成,它是内存分配的中心枢纽,一位大管家。包括管理 PoolChunk 对象、管理 PoolSubpage 对象、分配内存对象的核心逻辑、管理本地对象缓存池、内存池销毁等等,它的侧重点在于管理*已分配的内存对象。而 PoolChunk 是 jemalloc 算法思想的化身,它知道如何有效分配内存,你只需要调用对应方法就能获取想要大小的内存块,它只专注管理物理内存这件事情,至于分配后的事情,它一概不知,也一概不管,反正 PoolArena 这个大管家会操心的。
接下来,我们会通过 PooledByteBufAllocator 相关方法为入口,通过源码带你走进 Netty 分配内存的世界。堆外内存分配源码实现
堆外内存底层数据存储容器是 java.nio.ByteBuffer 对象。一般通过 io.netty.buffer.AbstractByteBufAllocator#directBuffer(int) 得到一个池化的堆外内存 ByteBuf 对象。跟踪方法,它会通过抽象类 io.netty.buffer.AbstractByteBufAllocator#newDirectBuffer 交给子类实现,这里是使用池化的分配器 PooledByteBufAllocator 实现。相关源码如下:
// io.netty.buffer.PooledByteBufAllocator#newDirectBuffer /** * 获取一个堆外内存的「ByteBuf」对象 */ @Override protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { // #1 从本地线程缓存中获取「PoolThreadCache」对象 PoolThreadCache cache = threadCache.get(); // #2 从缓存对象中获取「directArena」,根据存储类型不同选取对应的「Arena」 PoolArena<ByteBuffer> directArena = cache.directArena; final ByteBuf buf; if (directArena != null) { // #3-1 委托「directArena」完成内存分配 buf = directArena.allocate(cache, initialCapacity, maxCapacity); } else { // #3-2 兜底方案 buf = PlatformDependent.hasUnsafe() ? UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) : new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity); } // #4 包装生成好的「ByteBuf」对象,用于内存泄漏检查 return toLeakAwareBuffer(buf); }
上面就是分配器分配一个池化 ByteBuf 对象的核心源码。是不是感觉很简单,因此内存分配委托 directArena 完成的。之前说过,每个线程只能绑定一个 PoolArena 对象,在整个线程的生命周期内只和这个 PoolArena 打交道,而这个引用是存放在 PoolThreadCache 本地线程缓存里面,某个线程想要分配内存,调用 threadCache.get() 会初始化相关变量,一般 Netty 默认开始本地线程缓存,因此,从 cache 获得 directArena 对象不为空。这个 PoolThreadCache 可有用了! 它持有 PoolArena 对象,通过 MemoryRegionCache 缓存部分 ByteBuffer 或 byte[] 信息,这里我们只需要知道是从 PoolThreadCache 本地缓存中获取其中一个 dicrectArena 对象,通过比较 PoolByteBufAllocator 中每一个 PoolArena#numThreadCaches 大小,返回最小值的 PoolArena 对象。每个线程都拥有 PoolThreadCache。关于 PoolThreadCache 会在新的章节详细介绍。
继续跟着主线,现在执行到 PoolArena#allocate(PoolThreadCache, int, int)。那我们看看 PoolArena 作了些什么:阶段一: 初始化一个 ByteBuf 实例对象
通过对象池加速 ByteBuf 对象的内存和释放,但不好的一面是有如果对 Netty 底层不了解的开发人员的程序可能导致内存泄漏。如果对象池没有,则直接根据相应规则创建。 ```java // io.netty.buffer.PoolArena#allocate(io.netty.buffer.PoolThreadCache, int, int) /**
获取池化的「ByteBuf」实例 */ PooledByteBuf
allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) { // #1 获取一个「ByteBuf」实例对象。可能直接生成,也有可能从对象池中获取。 // 它是「PoolArena」抽象类,需要子类实现,这里是「PoolArena」实现类 PooledByteBuf
buf = newByteBuf(maxCapacity); // #2 为「buf」填充物理内存信息 allocate(cache, buf, reqCapacity);
// #3 返回 return buf; }
// io.netty.buffer.PoolArena.DirectArena#newByteBuf /**
获取一个「ByteBuf」实例对象。 */ @Override protected PooledByteBuf
newByteBuf(int maxCapacity) { if (HAS_UNSAFE) { // #1 带有「Unsafe」的「ByteBuf」,一般在服务器中都支持 Unsafe // 所以我们仔细看看这个方法是如何实现的 return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);
} else {
// #2 「非Unsafe」的「ByteBuf」 return PooledDirectByteBuf.newInstance(maxCapacity);
} }
- 「PooledUnsafeDirectByteBuf」没有被「public」修饰,它是包可见对象,因此,我们不能通过分配器获得此类型实例。
- 这个「ByteBuf」拥有「ObjectPool」对象池,可加速对象的分配效率。
- 还有一个和它类型的,叫「io.netty.buffer.PooledDirectByteBuf」,内部也使用「ObjectPool」对象池。
具体区别是「PooledUnsafeDirectByteBuf」内部维护「memoryAddress」变量,这是「Unsafe」操作的必要变量。 */ final class PooledUnsafeDirectByteBuf extends PooledByteBuf
{ // 对象池 private static final ObjectPool RECYCLER = ObjectPool.newPool( new ObjectCreator<PooledUnsafeDirectByteBuf>() { @Override public PooledUnsafeDirectByteBuf newObject(Handle<PooledUnsafeDirectByteBuf> handle) { return new PooledUnsafeDirectByteBuf(handle, 0); }
});
static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
// #1 从对象池中获取「ByteBuf」实例 PooledUnsafeDirectByteBuf buf = RECYCLER.get(); // #2 重置 buf.reuse(maxCapacity); // 返回 return buf;
}
private long memoryAddress;
// 重置所有指针变量 final void reuse(int maxCapacity) {
maxCapacity(maxCapacity); resetRefCnt(); setIndex0(0, 0); discardMarks();
}
阶段二: 为 ByteBuf 填充内存信息
这个阶段的核心方法属于 io.netty.buffer.PoolArena#allocate(io.netty.buffer.PoolThreadCache, io.netty.buffer.PooledByteBuf
, int) ,PoolArena 依据申请内存大小采用不同的内存分配策略,并把内存信息写入 ByteBuf 对象。前面我们对 PoolSubpage [] tinySubpagePools 和 PoolSubpage [] smallSubpagePools 这两个变量有所了解,会在分配 tiny&small 级别内存时使用到。待下次请求分配同等大小的内存时就可以通过现成的 PoolSubpage [] 进行分配。从源码好好休会一下: ```java // io.netty.buffer.PoolArena#allocate /**
- 分配物理内存,并为「buf」填充内存信息
- @param cache 当前线程绑定的「PoolThreadCache」
- @param buf 全新的「ByteBuf」对象
@param reqCapacity 申请内存容量大小 */ private void allocate(PoolThreadCache cache, PooledByteBuf
buf, final int reqCapacity) { // #1 规格化(详见「内存规格化」小节) final int normCapacity = normalizeCapacity(reqCapacity);
// #2 根据「normCapacity」采取不同分配策略 if (isTinyOrSmall(normCapacity)) {
// #2-1 「tiny」、「small」级别内存分配策略(capacity < pageSize) // 需要把「page」拆分成若干个「subpage」。因此核心步骤如下: // ① 根据对应的内存规格类型尝试从「PoolSubpage」获取内存大小一样的「subpage」 // 如果有,标记已使用并直接返回 // ② 在最底层找到空闲的「page」 // ② 将「page」拆分成等大(大小: pagesize/normCapacity)的「subpage」 // ③ 选取第一个用于当前内存块,其余添加到对应的「PoolSubpage」 // #1 根据内存规格(tiny、small)确定「tableIdx」和「table」 int tableIdx; // tiny=>tinySubpagePools,small=>smallSubpagePools PoolSubpage<T>[] table; boolean tiny = isTiny(normCapacity); if (tiny) { // < 512 // #2-1 尝试从「PoolThreadCache」本地线程缓存中分配内存,如果有,那最好了,直接返回 if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) { return; } // #2-2 根据内存大小确定「tinySubpagePools」数组的索引值 // 如上图所示: 16B对应0,32B对应1。 tableIdx = tinyIdx(normCapacity); table = tinySubpagePools; } else { // 与上述同理 if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) { return; } tableIdx = smallIdx(normCapacity); table = smallSubpagePools; } // #3 获取PoolSubpage[tableIdx]的头部节点 final PoolSubpage<T> head = table[tableIdx]; // 由于是共享变量,因此需要上锁。从这里也可以看出,PoolSubpage[]可减少锁的粒度,提高并发 synchronized (head) { // 获取下一个节点值 final PoolSubpage<T> s = head.next; // PoolSubpage初始化比较有意思,初始化时会将prev和next指针指向自己 // 如果为初始化状态,s==head是成立的。 if (s != head) { // 表明容量「normCapacity」能在「PoolSubpage」找到分配过的「subpage」 // 那就从这里分配吧! // 保险起见,判断是否被销毁了以及「elemSize」是否相等 assert s.doNotDestroy && s.elemSize == normCapacity; // # 4-1 从「PoolSubpage」对象中获取一块内存 long handle = s.allocate(); assert handle >= 0; // #4-2 初始化「ByteBuf」对象,赋予内存信息 s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity); // #4-3 更新分配监控指标 incTinySmallAllocation(tiny); return; } } // 锁住「this」,这是一把重量级的锁 synchronized (this) { // #5 分配内存 allocateNormal(buf, reqCapacity, normCapacity); } // #6 更新分配次数 incTinySmallAllocation(tiny); return;
}
// #7 分配「Normal」级别内存 if (normCapacity <= chunkSize) {
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) { return; } synchronized (this) { allocateNormal(buf, reqCapacity, normCapacity); ++allocationsNormal; }
} else {
// #8 分配「Huge」级别内存 allocateHuge(buf, reqCapacity);
} }
/*
虽然名字上看起来是分配「Normal」级别内存,实际上「Tiny&Small」级别内存也可以通过此方法分配 */ private void allocateNormal(PooledByteBuf
buf, int reqCapacity, int normCapacity) { // #1 按q050->q025->q000->qInit->q075顺序挨个尝试,直到成功分配并返回 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;
}
// #2 如果上述不能分配成功,那只能新创建一个「PoolChunk」对象 // 这是个抽象方法,基于特定子类实现。对于「PoolChunk」,会实例化一个 // 「java.nio.ByteBuffer」对象用做数据容器。 PoolChunk
c = newChunk(pageSize, maxOrder, pageShifts, chunkSize); // #3 再尝试分配 boolean success = c.allocate(buf, reqCapacity, normCapacity); assert success;
// #4 添加到「PoolChunkList」链表 qInit.add(c); }
/**
- @param pageSize 页大小
- @param maxOrder 树最大的层数
- @param pageShifts 页偏移量。根据「pageSize」计算得到,
计算公式: Integer.SIZE - 1 - Integer.numberOfLeadingZeros(pageSize);
- @param chunkSize 申请的「PoolChunk」容量大小,一般为16MB
@return */ @Override protected PoolChunk
newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize) {
// #1 判断是否需要内存对齐 if (directMemoryCacheAlignment == 0) {
// #1-1 直接通过「allocateDirect」分配「chunkSize」的「ByteBuffer」对象 // 使用「PoolChunk」包装 return new PoolChunk<ByteBuffer>(this, allocateDirect(chunkSize), pageSize, maxOrder, pageShifts, chunkSize, 0);
}
// #1-2 需要内存对齐,加上填充数即可 // offsetCacheLine(ByteBuffer) 计算偏移量 final ByteBuffer memory = allocateDirect(chunkSize + directMemoryCacheAlignment); return new PoolChunk
(this, memory, pageSize, maxOrder, pageShifts, chunkSize, offsetCacheLine(memory));
}
/*
- 获取「java.nio.ByteBuffer」对象
根据是否存在「Cleaner」回收对象而返回对应的「ByteBuf」类型 */ private static ByteBuffer allocateDirect(int capacity) { return PlatformDependent.useDirectBufferNoCleaner() ?
// 对于「无Cleaner」情况,先通过「Unsafe」分配内存块,然后通过反射构造「ByteBuffer」对象 PlatformDependent.allocateDirectNoCleaner(capacity) : // 直接「new」 ByteBuffer.allocateDirect(capacity);
} ``` 现在总结一下堆外内存分配逻辑:
- 首先,对申请容量进行规格化处理。获取最接近且大于原值的2的幂次方的值,称为规范值。
- 根据规范值选择合适的分配策略。从大方向讲,有 3 种分配策略,分别是 tiny&small、normal 以及 Huge。
- Huge 进行内存分配并不会尝试从本地线程缓存分配,也不会对它进行池化管理,直接创建 PoolChunk 对象并返回。
- 当 Normal 进行内存分配,会按 q050->q025->q000->qInit->q075 顺序进行分配,从 q050 开始分配是因为这是一个折中的分配方案,如果从 q000 分配的话,会有大部分的 PoolChunk 面临频繁的创建和销毁,造成内存分配的性能降低。如果从 q050 开始,会使 PoolChunk 的使用率范围保持在中间水平,既降低了 PoolChunkList 被回收的概率,也兼顾了性能。如果分配成功,则计算该 PoolChunk 的使用率,使用率超过了 PoolChunkList 的上限时,移动到下一个 PoolChunkList 链表中。如果分配失败,则会创建一个新的内存块进行内存,如果分配成功添加到 qInit 链表。
- 对于 Tiny&Small 级别,会尝试通过 PoolSubpage 分配,如果分配成功则返回。如果分配失败,则还是按 Normal 那套分配逻辑进行分配。
总的来说,PoolArena#allocate 方法是 PoolArena 对象分配内存的核心逻辑,会根据规范值选择合适的分配策略。而且通过本地线程缓存加速内存分配,通过对象池加速 ByteBuf 对象分配,并减少 GC。
堆内内存分配概述
堆内内存和堆外内存分配逻辑大致相同,不同点在于:
- 使用 PoolArena 的子类 HeapArena 完成分配工作。
- 底层数据容器为 byte[],而 DirectArena 是 java.nio.ByteBuffer 对象。
内存回收
内存回收需要分清楚主语是谁?我们知道,Netty 通过 Thead Cache 缓存部分已分配的内存,那么它是如何进行内存回收呢?这里的主语是 Thread Cache。而对于大管家 PoolArena,它是如何管理内存的回收?
众所周期,通过 BytBuf#release() 释放 ByteBuf 对象,这个 API 只会让引用计数值 -1,并非直接回收物理内存。只有当引用计数值为 0 再进行物理内存回收动作。
ByteBuf#release() 调用过程概述如下:
我们通过 Update 对象更新引用计数,如果引用计数为0,则需要释放内存。如果所属的「PoolChunk」不支持池化,则直接释放。对于可池化的「PoolChunk」,首先看能不能通过本地线程缓存待回收的内存信息,如果本地线程缓存成功,则返回。否则交给「PoolArena」处理内存回收。
「PoolArena」会交给所在的「PoolChunkList」链表进行处理。处理逻辑相对简单: 找到「PoolChunk」回收内存,判断「PoolChunk」是否满足 minUsage,不满足则移动前向节点。至此,这就是内存回收大致情况。 ```java // io.netty.buffer.AbstractReferenceCountedByteBuf#release() @Override public boolean release() { // #1 首先通过 updater 更新「refCnt」的值,refCnt=refCnt-2 // 如果旧值「refCnt」==2,则update.release(this)会返回true,表示当前「ByteBuf」引用计数为0了, // 是时候需要释放了 // #2 释放内存 return handleRelease(updater.release(this)); }
// io.netty.buffer.AbstractReferenceCountedByteBuf#handleRelease private boolean handleRelease(boolean result) { if (result) { // 释放内存 deallocate(); } return result; }
// io.netty.buffer.PooledByteBuf#deallocate @Override protected final void deallocate() { // 判断句柄变量是否>=0 if (handle >= 0) { final long handle = this.handle; this.handle = -1; memory = null;
// △ 使用「PoolArena#free」释放
chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
tmpNioBuf = null;
chunk = null;
// 回收「ByteBuf」对象
recycle();
}
}
// io.netty.buffer.PoolArena#free /**
- 由「PoolArena」定义「释放」二字
- @param chunk 「ByteBuf」所以的「PoolChunk」
- @param nioBuffer 「ByteBuf」内部的临时「ByteBuffer」对象
- @param handle 句柄变量值
- @param normCapacity 申请内存值
@param cache 线程缓存 */ void free(PoolChunk
chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
if (chunk.unpooled) {
// #1 待回收「ByteBuf」所属的「Chunk」为非池化,直接销毁 // 根据底层实现方式不同采取不同销毁策略。 // 如果是「ByteBuf」对象,根据有无「Cleaner」分类,采取不同的销毁方法 // 如果是「byte[]」,不做任何处理,JVM GC 会回收这部分内存 int size = chunk.chunkSize(); destroyChunk(chunk); activeBytesHuge.add(-size); deallocationsHuge.increment();
} else {
// #2 对于池化的「Chunk」 SizeClass sizeClass = sizeClass(normCapacity); if (cache != null && // 尝试添加到本地缓存,至于如何添加,会在另一章节详细说明 // 内部会使用「MermoryRegionCache」缓存内存信息,比如句柄值,容量大小、属于哪个「chunk」等 // 待后面这个线程申请等容量大小时就可以从本地线程中分配 // 那有人会说,有借不还么?那是不可能的,PoolThreadCache会维持添加计数,达到某个阈值则会触发 // 回收动作,并不会造成内存泄漏 cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) { return; } // 本地缓存添加失败,那就交给由「PoolArena」完成释放 freeChunk(chunk, handle, sizeClass, nioBuffer, false);
} }
// io.netty.buffer.PoolArena#freeChunk /**
- 释放「ByteBuf」对象
- @param chunk
- @param handle
- @param sizeClass
- @param nioBuffer
- @param finalizer
*/
void freeChunk(PoolChunk
chunk,
final boolean destroyChunk; synchronized (this) {long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) {
} if (destroyChunk) {// We only call this if freeChunk is not called because of the PoolThreadCache finalizer as otherwise this // may fail due lazy class-loading in for example tomcat. // 这里应对懒加载所做出的判断。比如「Tomcat」卸载某个应用时,会把对应的「ClassLoader」卸载掉, // 但对于线程回收finalizer而言可能需要这个类加载器的类信息,因此这里判断一下 if (!finalizer) { switch (sizeClass) { case Normal: ++deallocationsNormal; break; case Small: ++deallocationsSmall; break; case Tiny: ++deallocationsTiny; break; default: throw new Error(); } } // 调用PoolChunkList#free方法归还内存 destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);
} }// destroyChunk not need to be called while holding the synchronized lock. destroyChunk(chunk);
// io.netty.buffer.PoolChunkList#free
boolean free(PoolChunk
// #1 先通过「PoolChunk#free」回收内存块
// 「handle」记录树的位置信息
// 「PoolChunk」会缓存nioBuffer对象,用于下次体时使用
chunk.free(handle, nioBuffer);
// #2 判断当前「PoolChunk」的使用率,是否需要移到前一个节点链表中
if (chunk.usage() < minUsage) {
remove(chunk);
// Move the PoolChunk down the PoolChunkList linked-list.
return move0(chunk);
}
return true;
总结
以上是我们迈向 Netty 内存的一小步,也是熟悉 Netty 内存的一大步。2333,希望通过对特定的类、结构的分析让大家对整个内存流程有大致的了解。等熟悉这些过程后,我们再深究细节。