recycler 是 netty 专为高频短生命周期对象设计的线程局部对象复用机制,通过线程独占栈缓存回收对象,避免锁竞争和 gc;它不管理堆外内存,仅减少 new 对象导致的 young gc 压力。

Recycler 是什么,为什么它能缓解 GC 压力
Recycler 不是通用对象池框架,而是 Netty 专为高频、短生命周期对象(如 ByteBuf、ChannelPromise、DefaultHttpHeaders)设计的线程局部对象复用机制。它不依赖堆外内存管理,而是通过每个线程独占的栈(Stack)缓存已回收对象,避免锁竞争和跨线程引用,从而实现近乎零分配——只要对象被正确 recycle(),就不会进入 GC 生命周期。
关键点在于:GC 压力主要来自频繁 new 对象 + 短命对象大量进入 Young Gen → 频繁 YGC。Recycler 把这部分对象“扣”在栈里反复用,相当于把 new MyObject() 替换为 RECYCLER.get(),而回收动作只是压栈,不触发任何 GC。
如何让自定义对象接入 Recycler(以 ByteBuf 子类为例)
Netty 内置的 PooledByteBuf 已默认使用 Recycler,但如果你封装了自定义协议对象(比如 MyPacket),想复用其生命周期,必须显式集成 Recycler。常见错误是只重写 recycle() 却没传入 Handle,导致对象永远无法归还池中。
- 继承
Recycler<t></t>的静态内部类Handle<t></t>,并在构造时保存该 handle - 对象内部必须持有
Recycler.Handle<mypacket></mypacket>字段(不能丢弃) -
recycle()方法内必须调用handle.recycle(this),且仅能调用一次 - 禁止在
recycle()后继续访问该对象字段(Recycler 不做状态清零,需手动重置或依赖构造器初始化)
示例片段:
public class MyPacket {
private final Recycler.Handle<mypacket> handle;
private int length;
private MyPacket(Recycler.Handle<mypacket> handle) {
this.handle = handle;
}
static final Recycler<mypacket> RECYCLER = new Recycler<mypacket>() {
@Override
protected MyPacket newObject(Handle<mypacket> handle) {
return new MyPacket(handle);
}
};
public void recycle() {
if (handle != null) {
handle.recycle(this); // 这一行漏掉就等于没池化
}
}
}</mypacket></mypacket></mypacket></mypacket></mypacket>
DEFAULT_MAX_CAPACITY 和线程本地栈大小怎么设才不翻车
Recycler 默认每个线程栈最多缓存 DEFAULT_MAX_CAPACITY = 4096 个对象,看似很大,但实际要结合你的 QPS、平均处理时长、线程数来算真实水位。设太高会浪费内存(尤其对象本身较大),设太低会导致频繁 new/finalize,反而加剧 GC。
典型误配场景:
- 单线程处理 10k QPS、平均耗时 5ms → 每秒最多活跃对象 ≈ 10000 × 0.005 = 50 个 →
maxCapacity=256足够 - 使用
EventLoopGroup默认线程数(CPU×2)时,总缓存上限 = 线程数 ×maxCapacity,别盲目调高单线程容量 - 若发现
io.netty.util.Recycler$WeakOrderQueue对象持续增长,说明回收链路卡住(比如跨线程调用recycle()),不是容量问题,是用法错误
可通过 JVM 参数 -XX:+PrintGCDetails 对比开启 Recycler 前后 YGC 频次,再配合 jdk.jfr 观察 ObjectAllocationInNewTLAB 事件下降幅度,才是真实收益。
ByteBuf 用对了 Recycler,但 GC 还是高?检查这几个地方
Recycler 只管对象实例复用,不管底层内存。即使 PooledByteBuf 被池化,如果每次申请的内存块尺寸波动大、或长期 hold 大块内存不释放,依然会触发 PoolArena 的碎片整理甚至 Full GC。
- 确认是否启用了池化堆外内存:
UnpooledByteBufAllocator不走 Recycler,必须用PooledByteBufAllocator - 避免频繁调用
capacity(int)或ensureWritable(int)导致内存扩容,优先预估并复用固定 size 的ByteBuf - 检查是否有未
release()的ByteBuf—— Recycler 不负责引用计数释放,那是ReferenceCounted的事;泄露一个ByteBuf就可能拖垮整个 arena - 注意
CompositeByteBuf不参与 Recycler,它的组件ByteBuf才是池化目标
最隐蔽的问题:你在 handler 中 ctx.write(msg) 后立刻 msg.release(),但 Netty 异步写入尚未完成,此时对象已被回收,后续 write 操作可能读到脏数据或抛 IllegalReferenceCountException —— 正确做法是用 ctx.writeAndFlush(msg).addListener(future -> msg.release())。










