说说 Netty 的对象池技术?

#中间件#面试八股

说说 Netty 的对象池技术?

结论先行:Netty 有两套对象池。一套是 Recycler,池化任意 Java 对象(如 ByteBuf 的包装、ChannelOutboundBuffer 里的 Entry);另一套是 PooledByteBufAllocator,按 jemalloc 思路把堆外内存切成 arena → chunk → page → subpage 四级来分配。前者省 GC,后者省系统调用与内存碎片。

一、两层池化

层次组件池化对象解决的问题
对象池Recycler普通 Java 对象减少分配与 GC
内存池PooledByteBufAllocatorByteBuf 的底层内存减少 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 与分配开销,代价是必须严格管理引用计数。
笔记加载中…