netty的recycler无法保证零gc,但正确使用可使小对象gc压力趋近于零;需满足生命周期短、高频创建、构造开销大、状态可重置、不跨线程逃逸等条件,并严格实现newobject与recycle。

Netty 的 Recycler 本身不提供“零 GC”的绝对保证,但它能在绝大多数高吞吐场景中把小对象的 GC 压力压到趋近于零——前提是正确使用、避免跨线程误回收、控制好容量阈值。
Recycler 不是万能的:哪些对象适合池化
它只对满足以下全部条件的小对象有效:
- 生命周期短、创建/销毁频率极高(如每秒数万次)
- 构造开销明显(字段初始化、集合预分配等)
- 逻辑状态可安全重置(
recycle()调用前必须清空业务字段) - 不逃逸出创建线程(即不被其他线程长期持有或共享)
典型例子:HttpRequest / HttpResponse 包装器、自定义协议帧解析器、事件上下文 HandlerContext 辅助对象。反例:含外部引用的回调闭包、带 ThreadLocal 引用的单例包装器、未重写 recycle() 的子类。
必须重写 newObject(Handle) 和 recycle()
直接继承 Recycler 并不自动复用对象;你得自己管理对象状态。关键点:
-
newObject(Handle)返回新实例,但不要做任何业务初始化(留到首次使用时) -
recycle()必须显式重置所有字段(包括集合clear()、引用置null),否则下次get()拿到的是脏数据 - 不要在
recycle()中调用super.recycle()—— 它已由Recycler内部完成
示例片段:
public class MyEvent extends Recycler.MyObject {
private String id;
private int code;
private MyEvent(Recycler<myevent> recycler) {
this.recycler = recycler;
}
public static MyEvent newInstance() {
return RECYCLER.get();
}
@Override
protected void recycle() {
this.id = null; // 必须清空
this.code = 0;
this.recycler.recycle(this); // 注意:不是 super.recycle()
}
private static final Recycler<myevent> RECYCLER = new Recycler<myevent>() {
@Override
protected MyEvent newObject(Handle<myevent> handle) {
return new MyEvent(handle);
}
};
}</myevent></myevent></myevent></myevent>
线程本地栈溢出与 DEFAULT_MAX_CAPACITY
每个线程的 Stack 默认最大容量是 Recycler.DEFAULT_MAX_CAPACITY = 4096。超过后新对象不再入栈,直接走 new 分配 → 瞬间引入 GC。
- 若压测发现 P99 延迟突增且 GC 日志出现大量
Allocation Failure,先查是否栈满 - 可通过
-Dio.netty.recycler.maxCapacityPerThread=8192调整,但别盲目拉高——内存占用线性增长,且缓存局部性下降 - 更推荐方式:结合监控,在业务低峰期用
Stack.size()(需反射访问)采样各线程实际占用,按 95 分位设上限
跨线程回收导致 WeakOrderQueue 积压
当 A 线程创建的对象被 B 线程调用 recycle(),它会进入 B 线程的 WeakOrderQueue,等待下一次 A 线程调用 get() 时被迁移回栈。但如果 A 线程长期空闲,队列就堆积 → 内存泄漏风险 + 回收延迟。
- 禁止在 IO 线程外(如业务线程池)调用
recycle() - 若必须跨线程释放(如异步回调),改用
PooledByteBufAllocator配合ByteBuf.release(),它有更健壮的跨线程引用计数机制 - 可通过 JVM 参数
-Dio.netty.recycler.delayedQueueRatio=2加快迁移频率(默认是 1/8 次 get 触发一次迁移)
真正难处理的从来不是怎么写 Recycler,而是让所有开发者都记得:每次 get() 后必须 recycle(),且不能跨线程乱传对象引用——这点靠代码审查和单元测试覆盖比靠文档更可靠。










