基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

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)。
    • PoolArenasmallSubpagePools 数组(一个存放 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)。
  • 工作流
    1. 线程申请内存时,首先检查自己的 PoolThreadCache
    2. PoolThreadCache 内部按照 Small, Normal 等规格维护了多个 MemoryRegionCache(队列)。
    3. 如果本地缓存有空闲的内存块,直接分配,全程无锁,速度极快。
    4. 如果本地缓存没有,再去 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 实现了极其彪悍的内存分配性能,这也是它能支撑千万级并发的核心基石之一。

00:00
00:00