15.Netty的对象池Recycler

总结
  • 对象池通过复用对象避免频繁 GC,适合高频创建/销毁的短生命周期对象场景
  • 核心结构:Stack(本线程回收)+ WeakOrderQueue(异线程回收)+ Link + DefaultHandle
  • 获取对象:优先从 Stack 弹出,Stack 空了就从 WeakOrderQueue 迁移
  • 回收对象:同线程直接 pushNow 到 Stack,异线程 pushLater 到 WeakOrderQueue
  • 对象和线程绑定,不管哪个线程回收,最终都归还到创建它的线程的 Stack
  • 回收有速率控制,每 8 个对象只回收 1 个,防止对象池无限膨胀

1. 什么时候用对象池?

对象池适合这类场景:

用了对象池之后的好处:

Netty 的 ByteBufChannel 相关对象都大量使用了 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
    }
}

有两点要注意:

  1. 对象池和线程绑定:线程 A 回收的对象,线程 B 直接 get() 是拿不到的,因为两个线程各自有独立的 Stack
  2. 对象和创建线程绑定:不管哪个线程回收,对象最终都会归还到创建它的那个线程的 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 有四个核心组件:StackWeakOrderQueueLinkDefaultHandle
内部结构
各组件关系:
组件关系
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() 时再统一迁移过来。

availableSharedCapacityAtomicInteger 是因为 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 的引用。
20260319101947

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);
}

WeakOrderQueue
迁移时会用 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)

  1. 从当前线程的 DELAYED_RECYCLEDFastThreadLocal 维护的 Map)里找目标 Stack 对应的 WeakOrderQueue
  2. 没有就新建一个,并把 Stack 的 head 指向它
  3. 每个线程最多帮助 2 * CPU核数 个线程回收,超过就放入 WeakOrderQueue.DUMMY 标记,后续直接丢弃
  4. 把对象写入 WeakOrderQueue 的 Link 链表尾部

对象写入 WeakOrderQueue 后,handle.stack 会被置为 null,等迁移回 Stack 时再重新赋值。这是为了防止 Stack 因为被 handle 强引用而无法被 GC 回收。