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