说说 Netty 的对象池技术?
结论先行:Netty 有两套对象池。一套是 Recycler,池化任意 Java 对象(如 ByteBuf 的包装、ChannelOutboundBuffer 里的 Entry);另一套是 PooledByteBufAllocator,按 jemalloc 思路把堆外内存切成 arena → chunk → page → subpage 四级来分配。前者省 GC,后者省系统调用与内存碎片。
一、两层池化
| 层次 | 组件 | 池化对象 | 解决的问题 |
|---|
| 对象池 | Recycler | 普通 Java 对象 | 减少分配与 GC |
| 内存池 | PooledByteBufAllocator | ByteBuf 的底层内存 | 减少 malloc/free 与碎片 |
| 线程本地 | FastThreadLocal | 线程私有变量 | 减少 ThreadLocal 哈希冲突 |
二、Recycler 的结构
Recycler<T>
├── FastThreadLocal<Stack<T>> 每个线程一个栈,同线程回收/获取最快
│ └── DefaultHandle[] 数组栈,默认容量 256
└── WeakOrderQueue 跨线程回收队列
├── 线程 A 回收的对象 → 投递到队列
└── 拥有者线程取用时从队列批量搬运(link)
同线程分配与回收走无锁数组栈;跨线程回收不直接操作别人的栈,而是投递到 WeakOrderQueue,拥有者线程空闲时批量拉取,避免锁竞争。
public class Conn implements Recycler.Handle<Conn> {
private static final Recycler<Conn> POOL = new Recycler<Conn>() {
@Override
protected Conn newObject(Handle<Conn> handle) {
return new Conn(handle);
}
};
private final Recycler.Handle<Conn> handle;
private Conn(Recycler.Handle<Conn> handle) { this.handle = handle; }
public static Conn obtain() { return POOL.get(); }
public void recycle() { handle.recycle(this); }
}
三、内存池的四级结构
PooledByteBufAllocator
└── PoolArena[] 默认 2 × CPU 核数个,按线程哈希绑定
└── PoolChunk 默认 16MB,管理一棵完全二叉树
└── PoolPage 默认 8KB
└── PoolSubpage 小于 page 的请求(如 16B)在页内再切
| 请求大小 | 分配路径 | 说明 |
|---|
| < 8KB(tiny/small) | Subpage | 页内切块,按尺寸分级 |
| 8KB ~ 16MB(normal) | Page 二叉树 | 按伙伴算法找最小可用块 |
| > 16MB(huge) | 直接分配 | 不进池,用完即还 |
四、配置
// 全局指定池化分配器(4.1 默认已是 pooled)
b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
// 自定义 arena 数与页大小
PooledByteBufAllocator alloc = new PooledByteBufAllocator(
true, // preferDirect
8, // nHeapArena
8, // nDirectArena
8192, // pageSize
11, // maxOrder,chunk 大小 = pageSize << maxOrder = 16MB
0, // tinyCacheSize,0 表示用默认值
0, // smallCacheSize
0, // normalCacheSize
0); // maxCachedBufferCapacity
相关系统属性:
-Dio.netty.allocator.numDirectArenas=8
-Dio.netty.allocator.pageSize=8192
-Dio.netty.allocator.maxOrder=11
-Dio.netty.allocator.tinyCacheSize=512
五、池化的代价
| 代价 | 表现 | 缓解 |
|---|
| 泄漏 | 忘了 release,池化内存只增不减 | 泄漏检测 + 引用计数审计 |
| 内存驻留 | 池不会主动归还 OS,RSS 看起来很高 | 属正常现象,看堆外使用量而非 RSS |
| 碎片 | 大小混杂导致页利用率下降 | 统一协议帧大小,减少极端尺寸 |
常见追问
- 追问:为什么池化后 RSS 一直涨?内存池按 chunk 申请、按需复用,空闲时也不还给操作系统;只要
PooledByteBufAllocator 的 metric 显示在用内存稳定就没问题。
- 追问:
Recycler 和 ThreadLocal 有什么区别?Recycler 是为“对象复用”设计的,有跨线程回收队列与容量上限;ThreadLocal 只是线程私有存储,没有回收语义。
- 追问:什么时候不该用池化?对象很大且生命周期长、或者持有外部资源(连接、文件句柄)时,复用容易引入状态残留。
小结
Recycler 池化对象,PooledByteBufAllocator 池化内存,两者目标不同。
- 同线程走无锁栈,跨线程走 WeakOrderQueue 批量搬运。
- 内存池分 arena/chunk/page/subpage 四级,按请求大小选路径。
- 池化降低 GC 与分配开销,代价是必须严格管理引用计数。