15.Netty的对象池Recycler
- 对象池通过复用对象避免频繁 GC,适合高频创建/销毁的短生命周期对象场景
- 核心结构:Stack(本线程回收)+ WeakOrderQueue(异线程回收)+ Link + DefaultHandle
- 获取对象:优先从 Stack 弹出,Stack 空了就从 WeakOrderQueue 迁移
- 回收对象:同线程直接 pushNow 到 Stack,异线程 pushLater 到 WeakOrderQueue
- 对象和线程绑定,不管哪个线程回收,最终都归还到创建它的线程的 Stack
- 回收有速率控制,每 8 个对象只回收 1 个,防止对象池无限膨胀
1. 什么时候用对象池?
对象池适合这类场景:
- 高频创建/销毁的短生命周期对象:比如网络请求中的 ByteBuf、RPC 框架里的请求/响应对象,每次请求都 new 一个,用完就扔,GC 压力很大
- 对象创建成本高:初始化逻辑复杂、需要分配大块内存,或者涉及系统资源(如连接、文件句柄)
- 对象状态可以重置:用完之后能清理干净重新使用,不会带着脏数据
用了对象池之后的好处:
- 减少 GC 压力:对象不再频繁进入老年代,Full GC 次数明显下降,延迟更稳定
- 提升吞吐量:省掉了对象分配和初始化的开销,在高并发场景下效果尤其明显
- 内存更可控:池子大小有上限,不会因为瞬时流量导致内存暴涨
Netty 的 ByteBuf、Channel 相关对象都大量使用了 Recycler,这也是 Netty 在高并发下性能表现优秀的原因之一。
2. 快速上手
对象需要持有一个 Handle 引用,用于回收自身:
public class UserCache {
// 每个线程最多缓存 1024 个对象,默认是 4096
private static final Recycler<User> userRecycler = new Recycler<User>(1024) {
@Override
protected User newObject(Handle<User> handle) {
return new User(handle);
}
};
static final class User {
private String name;
private Recycler.Handle<User> handle;
public User(Recycler.Handle<User> handle) {
this.handle = handle;
}
public void recycle() {
handle.recycle(this);
}
// getter/setter 省略
}
public static void main(String[] args) {
User user1 = userRecycler.get(); // 从对象池获取
user1.setName("hello");
user1.recycle(); // 回收到对象池
User user2 = userRecycler.get(); // 再次获取,拿到的是 user1
System.out.println(user2.getName()); // hello
System.out.println(user1 == user2); // true
}
}
有两点要注意:
- 对象池和线程绑定:线程 A 回收的对象,线程 B 直接
get()是拿不到的,因为两个线程各自有独立的 Stack - 对象和创建线程绑定:不管哪个线程回收,对象最终都会归还到创建它的那个线程的 Stack 里
验证第二点的例子:
static User user = null;
public static void main(String[] args) throws InterruptedException {
// main 线程创建对象
user = User.getInstance();
Thread t1 = new Thread(() -> {
// t1 线程回收 main 线程创建的对象
user.recycle();
});
t1.start();
Thread.sleep(1000);
// main 线程再次获取,能拿到 t1 回收的那个对象
User user2 = User.getInstance();
System.out.println(user2 == user); // true
}
输出:
main 创建对象: User@8807e25
Thread-1 回收对象: User@8807e25
User@8807e25 // main 线程拿回了同一个对象
3. 内部结构
Recycler 有四个核心组件:Stack、WeakOrderQueue、Link、DefaultHandle。

各组件关系:

Stack 是对象池的顶层结构,每个线程持有一个,通过 FastThreadLocal 实现线程私有化。
static final class Stack<T> {
final Recycler<T> parent;
final WeakReference<Thread> threadRef; // 绑定线程的弱引用
final AtomicInteger availableSharedCapacity; // 异线程可回收的最大对象数,默认 16K
final int maxDelayedQueues; // WeakOrderQueue 最大个数
private final int maxCapacity; // 对象池最大容量,默认 4096
private final int ratioMask; // 回收比率控制,默认回收 1/8
private DefaultHandle<?>[] elements; // 存储对象的数组
private int size;
private int handleRecycleCount = -1;
private WeakOrderQueue cursor, prev;
private volatile WeakOrderQueue head; // WeakOrderQueue 链表头
}
WeakOrderQueue 存储其他线程回收过来的对象。比如 ThreadA 创建的对象被 ThreadB 回收,ThreadB 不会直接写 ThreadA 的 Stack(有锁竞争),而是写入 ThreadA 的 Stack 所维护的 WeakOrderQueue 链表里,等 ThreadA 下次 get() 时再统一迁移过来。
availableSharedCapacity 用 AtomicInteger 是因为 ThreadB、ThreadC 等多个线程可能同时往 ThreadA 的 WeakOrderQueue 写,需要并发控制。
Link 是 WeakOrderQueue 内部的链表节点,每个节点默认存 16 个对象,满了就新建一个 Link 追加到尾部。
LINK_CAPACITY = safeFindNextPositivePowerOfTwo(max(SystemPropertyUtil.getInt("io.netty.recycler.linkCapacity", 16), 16))
DefaultHandle 是对象的包装类,Stack 的 elements 数组和 Link 里存的都是它。每个 DefaultHandle 持有对象本身和所属 Stack 的引用。

4. 获取对象
public final T get() {
if (maxCapacityPerThread == 0) {
return newObject((Handle<T>) NOOP_HANDLE);
}
Stack<T> stack = threadLocal.get(); // 拿当前线程的 Stack
DefaultHandle<T> handle = stack.pop(); // 从 Stack 弹出
if (handle == null) {
handle = stack.newHandle();
handle.value = newObject(handle); // Stack 空了就新建
}
return (T) handle.value;
}
pop() 的逻辑:Stack 有对象就直接弹出;Stack 空了就调 scavenge() 尝试从 WeakOrderQueue 迁移一批过来,迁移成功再弹出,迁移失败就返回 null 触发新建。
boolean scavengeSome() {
// cursor 遍历 WeakOrderQueue 链表
do {
if (cursor.transfer(this)) { // 迁移成功就返回
success = true;
break;
}
WeakOrderQueue next = cursor.next;
if (cursor.owner.get() == null) {
// 线程已退出,把剩余数据全部迁移过来,然后从链表移除这个节点
if (cursor.hasFinalData()) {
for (;;) {
if (!cursor.transfer(this)) break;
}
}
if (prev != null) prev.setNext(next);
} else {
prev = cursor;
}
cursor = next;
} while (cursor != null && !success);
}

迁移时会用 dropHandle 控制回收频率,每 8 个对象只保留 1 个,其余丢弃,防止对象池无限膨胀:
boolean dropHandle(DefaultHandle<?> handle) {
if (!handle.hasBeenRecycled) {
if ((++handleRecycleCount & ratioMask) != 0) {
return true; // 丢弃
}
handle.hasBeenRecycled = true;
}
return false;
}
5. 回收对象
// DefaultHandle#recycle
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
stack.push(this);
}
void push(DefaultHandle<?> item) {
Thread currentThread = Thread.currentThread();
if (threadRef.get() == currentThread) {
pushNow(item); // 同线程:直接入栈
} else {
pushLater(item, currentThread); // 异线程:写入 WeakOrderQueue
}
}
同线程回收(pushNow):直接把对象压入 Stack 的 elements 数组。超过 maxCapacity 或触发回收频率控制就直接丢弃。数组满了就扩容到 2 倍(上限 maxCapacity)。
异线程回收(pushLater):
- 从当前线程的
DELAYED_RECYCLED(FastThreadLocal 维护的 Map)里找目标 Stack 对应的 WeakOrderQueue - 没有就新建一个,并把 Stack 的
head指向它 - 每个线程最多帮助
2 * CPU核数个线程回收,超过就放入WeakOrderQueue.DUMMY标记,后续直接丢弃 - 把对象写入 WeakOrderQueue 的 Link 链表尾部
对象写入 WeakOrderQueue 后,handle.stack 会被置为 null,等迁移回 Stack 时再重新赋值。这是为了防止 Stack 因为被 handle 强引用而无法被 GC 回收。