Netty 的内存池(PoolArena)是如何划分内存区域的?
Netty 的内存池设计受到了 Jemalloc 内存分配算法的深度启发,其核心目标是:在极高并发下减少锁竞争,并最大程度降低内存碎片。
Netty 的内存池(以 PoolArena 为核心)将内存划分为了多个层级和区域。我们可以从 分配单位(层级结构) 和 分配规格(内存大小) 两个维度来理解它的划分。
一、 核心层级划分(物理/逻辑结构)
Netty 的内存池从上到下构成了严格的层级结构:PoolArena -> PoolChunk -> Page -> PoolSubpage。
1. PoolArena (内存竞技场)
- 角色:整个内存池的管理者,相当于一个内存分发中心。
- 划分机制:为了减少多线程并发申请内存时的锁竞争,Netty 默认会创建多个
PoolArena(通常与 CPU 核心数相关,默认是 CPU 核数 × 2)。线程在第一次申请内存时,会被绑定到一个负载最轻的PoolArena上。 - 分类:分为
HeapArena(管理堆内存)和DirectArena(管理堆外直接内存)。
2. PoolChunk (内存块)
- 角色:向操作系统/JVM申请内存的基本单位。
- 大小:默认大小为 16MB。
- 内部结构(伙伴算法):
- 一个 Chunk 被划分为 2048 个 Page(页)。
- 内部使用一棵满二叉树(伙伴分配算法)来管理这些 Page。二叉树共有 12 层(0-11),第 11 层的叶子节点共有 2048 个,每个叶子节点代表一个 Page。这种设计能够极快地找到连续的空闲 Page,有效防止外部碎片。
3. Page (内存页)
- 角色:
PoolChunk内部的基本分配单位。 - 大小:默认大小为 8KB。
- 机制:当申请的内存大于等于 8KB 时,Arena 会直接在 Chunk 的二叉树中分配一个或多个连续的 Page 给用户。
4. PoolSubpage (子页)
- 角色:用于处理小于 8KB 的小内存分配。
- 机制:如果直接分配一个 8KB 的 Page 给一个小对象(比如只申请 16 字节),会造成巨大的内部碎片。因此,Netty 会把一个完整的 Page(8KB)按指定规格切分成多个相等大小的 Element(内存小块),由
PoolSubpage进行管理。 - 内部结构:使用 位图 (Bitmap) 来记录每个小块是空闲还是被占用。
二、 按申请尺寸的区域划分(规格分类)
当我们向 Netty 申请内存时,Netty 会根据申请容量(Capacity)的大小,将请求路由到不同的区域进行处理。
(注:Netty 4.1.45 之后,为了向 Jemalloc 4 靠拢,去掉了 Tiny 类别,将其合并入了 Small,以下基于现代版本的 Netty 讲解)
1. Small 内存区域(<= 8KB)
- 范围:申请的内存在
[16B, 8KB)之间。 - 分配策略:由 PoolSubpage 分配。
- Netty 会把请求的尺寸向上取整到最近的规格(例如:16B, 32B, 64B... 到 8KB)。
- 去
PoolArena的smallSubpagePools数组(一个存放PoolSubpage的数组)中找到对应规格的 Subpage。 - 通过 Bitmap 找到 Subpage 中空闲的小块(Element)分配出去。
2. Normal 内存区域(8KB ~ 16MB)
- 范围:申请的内存在
[8KB, 16MB]之间。 - 分配策略:由 PoolChunk 按 Page 分配。
- 同样向上取整(比如申请 9KB 会按 16KB 分配,即 2 个 Page)。
- 在
PoolChunk的二叉树(伙伴系统)中,寻找满足连续 Page 数量的节点分配给请求。 - Arena 中有多个
ChunkList(如q000,q025,q050,q075,q100),根据 Chunk 的内存使用率将它们分类,优先从使用率高的 Chunk 中分配,以提高内存利用率。
3. Huge 内存区域(> 16MB)
- 范围:申请的内存大于 16MB。
- 分配策略:不进入内存池。
- 对于这种超大内存,Netty 认为将其池化的成本高于收益。
- 直接向 JVM/操作系统 申请一块不受内存池管理的内存(Unpooled),使用完毕后直接销毁。
三、 极高并发的秘密:PoolThreadCache (线程本地缓存)
为了进一步减少去 PoolArena 申请内存时的锁竞争(哪怕 PoolArena 已经有多个了),Netty 引入了 PoolThreadCache。
- 机制:每个 Netty 的 I/O 线程内部,都有一个属于自己的本地缓存(类似
ThreadLocal)。 - 工作流:
- 线程申请内存时,首先检查自己的
PoolThreadCache。 PoolThreadCache内部按照 Small, Normal 等规格维护了多个MemoryRegionCache(队列)。- 如果本地缓存有空闲的内存块,直接分配,全程无锁,速度极快。
- 如果本地缓存没有,再去
PoolArena申请(此时才可能发生锁竞争),申请到之后,后续释放时也会优先放回本地缓存。
- 线程申请内存时,首先检查自己的
总结:Netty 内存区域划分全景图
你可以用下面这个简化的流程图在脑海中建立模型:
plaintext
请求分配内存 (size)
│
├─> size > 16MB (Huge)
│ └── 非池化分配,直接向 OS/JVM 申请。
│
└─> size <= 16MB (进入池化流程)
│
├─ 1. 尝试从 [线程本地缓存 PoolThreadCache] 中获取 (无锁操作) --> 成功则返回
│
└─ 2. 缓存未命中,进入绑定的 [PoolArena] 分配 (加锁操作)
│
├─ size >= 8KB (Normal)
│ └── 在 [PoolChunk] 的满二叉树中,分配 1 到多个连续的 [Page]
│
└─ size < 8KB (Small)
└── 在 PoolArena 的数组中找对应规格的 [PoolSubpage] (由 Page 切割而来),通过 Bitmap 分配小块。
通过这种宏观分 Arena、中观分 Chunk/Page、微观分 Subpage,以及配合线程本地缓存的设计,Netty 实现了极其彪悍的内存分配性能,这也是它能支撑千万级并发的核心基石之一。