一、背景与设计目标
目标:回答为什么要有 Recycler、它要解决什么问题、它的设计边界是什么,并用简短可运行的例子说明“如何用”。
1.1 背景:高性能网络框架为什么需要对象池?
网络 I/O 的“微对象”风暴
在 Netty 这类事件驱动的网络框架中,每次 I/O 读写、编解码、任务封装,都会产生大量生命周期极短的小对象(典型如 Runnable
/Promise
、小型状态对象、临时包装类、少量元数据容器等)。如果完全依赖 JVM 自动内存管理(不断 new
+ 快速进入/离开年轻代),有三个常见代价:
-
GC 压力:瞬时对象分配/回收产生频繁 Minor GC,吞吐和 Tail Latency(P99/P999)抖动明显。
-
逃逸与跨线程:对象在线程之间转手,可能在堆上分配,失去栈上/标量替换优化机会。
-
重复初始化开销:逻辑上“等价”的对象被反复构建与字段赋值,浪费 CPU Cache 友好度。
对象池的基本诉求
因此,Netty 引入了一个轻量级、以线程局部为优先策略的对象池——Recycler。它的目标不是“缓存所有对象”,而是用尽可能低的同步开销,把热路径上反复创建的小对象“复用”起来,降低 GC 和构造成本,同时避免成为新的瓶颈。
官方描述里,把 Recycler 定位为“基于线程本地栈(thread-local stack)的轻量级对象池”。这点在 4.1 分支 API 文档中有明确表述。
1.2 设计目标(Design Goals)
站在 Netty 的性能哲学上,Recycler 的设计核心可以概括为 8 点:
-
极致快速的“同线程回收→同线程再用”
-
Fast-path 基于每线程一个
Stack
的后进先出(LIFO)结构,实现无锁的推/弹。 -
大多数对象都在创建它的那个线程上被回收和再利用,避免跨线程同步。
-
-
可控的跨线程回收
-
如果对象在其他线程调用
recycle()
,不会直接入原线程的Stack
,而是进入一个弱引用(关联原线程)的单向链队列WeakOrderQueue
;原线程在下次get()
时将其批量转移回自己的Stack
。 -
这减少了跨线程竞争(“生产者线程”只在本地排队,不与目标线程同步入栈)。
-
-
容量/增长速率受控
-
通过系统属性限制容量、共享容量因子、以及“回收比率(
io.netty.recycler.ratio
)”来控制增长速度,避免突发流量把池子瞬间“吹爆”。在 4.1 代码中默认值常见为 8(每 8 次尝试允许一次 push),用于渐增池容量。
-
-
GC 友好
-
WeakOrderQueue
对所有者线程持有弱引用,线程终止可促进清理,不阻滞 GC。 -
分段结构(
Link
)避免单次转移过大内存块;Link
的数组容量存在固定上限(历史实现里常见 16),以减轻膨胀。 -
通过线程本地结构减少共享对象和跨代引用,提升 GC 局部性。
-
-
安全性:避免重复回收(double-free)
-
DefaultHandle
维护回收标记(recycleId
/lastRecycledId
等),防止同一实例被重复回收或被错误线程回收导致结构损坏。
-
-
可调参数→可操作性
-
提供系统属性(如
io.netty.recycler.maxCapacity.default
、io.netty.recycler.ratio
等)用于运维/压测下的阈值调优与“开关”控制。
-
-
按需启用/按类型隔离
-
不同类型(比如内部任务对象 vs. 用户类型)可以有不同的池(不同
Recycler
子类或不同容量),最大化命中率与隔离度。
-
-
简单 API,低侵入
-
使用者只需在自定义对象中保存一个
Recycler.Handle<T>
并在合适时机调用recycle(this)
,其余交由 Recycler 管理。
-
小结一句:Recycler 的核心是**“线程局部 LIFO 栈 + 弱引用跨线程队列 + 受控增长/容量 + 安全回收标记”**。这四板斧解决了“快、稳、省、准”的问题。
1.3 与“直接用 ThreadLocal 缓存对象”有什么不同?
很多同学会说:“我用 ThreadLocal<Deque<T>>
自己写个池不就好了?”差异主要在:
-
跨线程回收:单纯 ThreadLocal 无法优雅处理“在 A 线程创建、在 B 线程回收”的情况;Recycler 用
WeakOrderQueue
做“延迟转移”,既避免锁,又能最终回到创建者线程。 -
批处理与分段:
WeakOrderQueue
用Link
分段批转,降低频繁迁移成本和内存碎片;纯ThreadLocal
自己实现容易退化成大量小对象的频繁交互。 -
安全性机制:
DefaultHandle
的双重回收检测、回收比率、共享容量因子等**“护栏”**,减少错误使用导致的内存涨落。 -
可调参数 & FastThreadLocal:Recycler 内部大量用到 Netty 的
FastThreadLocal
/FastThreadLocalThread
,进一步优化 ThreadLocal 访问路径的性能特性。
1.4 关键术语与结构一览(为后续章节打地基)
-
Recycler<T>
:对象池总控,提供get()
/recycle()
的框架与钩子。 -
Stack<T>
(每线程一个):LIFO 栈,存放当前线程可直接复用的DefaultHandle
。 -
WeakOrderQueue
(跨线程队列):当 非拥有线程 调用recycle()
时,句柄会被放进这里;等拥有者线程下一次分配时再批量转回Stack
。-
内部由多个
Link
(固定容量数组) 串联组成,弱引用关联拥有线程;典型实现里Link
的数组容量为 16。
-
-
DefaultHandle
:池里真正存放的“句柄”(持对象引用和状态位);它既是“票据”,也是防止重复回收的安全阀。 -
FastThreadLocal
:Netty 自研的高性能 ThreadLocal,在FastThreadLocalThread
上访问更快。
注:这些名字都能在 4.0/4.1 源码中看到,属于 Recycler 的基本组成。
1.5 设计取舍(Trade-offs)
-
不是通用大而全的对象池:Recycler 的最佳收益来自小而热、可复用、生命周期短的对象。对“重量级对象/昂贵重置”的场景未必合适。
-
容量不是越大越好:大容量意味着更少分配,但也可能“囤积”过多未用对象/句柄,加大内存占用波动;因此提供
maxCapacity
/ratio
等调参“刹车”。(io.netty.recycler.maxCapacity.default
、io.netty.recycler.ratio
等,后文详解) -
跨线程回收是“延迟可见”:B 线程回收到 A 线程的对象不会立刻可用,只有 A 线程下次
get()
才会把WeakOrderQueue
的Link
批量转移回来——这是刻意的去同步化设计。 -
安全检测非零开销:
DefaultHandle
的标记与检查带来极小 CPU 成本,换来强安全性(避免双重回收/交叉回收)。
1.6 文字流程图:两条快车道 + 一条辅路
(1)同线程“快车道”
Thread A:
get():
├─ A.Stack 非空 → O(1) 出栈句柄 → 复用对象
└─ A.Stack 为空 → 尝试从 A.Stack 绑定的 WeakOrderQueues 批量转入
→ 若仍无 → new 对象 + new DefaultHandle
recycle():
└─ 来自 Thread A → 直接 push 到 A.Stack(受 ratio/容量限制)
(2)异线程“辅路”
Thread B:
recycle(obj from A):
└─ 找到 (A.Stack) 对应的 WeakOrderQueue(与 A 线程弱关联)
→ 将 DefaultHandle 放入 B 本地构建/获取的 Queue 的尾部 Link
→ 不锁 A!只是在 B 的队列里排队
Thread A:
get()(后续某次):
└─ 扫描并批量从 WeakOrderQueue 的 Link 转移句柄到 A.Stack,再出栈使用
这个“延迟批转 → 少同步”是 Recycler 把跨线程场景做顺滑的关键。
1.7 极简可运行示例(掌握使用姿势)
目的:你能“看懂并敲出来”,理解 Handle 的角色与
recycle(this)
的契约。示例只演示 API 使用,不依赖 Netty 其它模块。
import io.netty.util.Recycler;
public class PooledObject {
// 关键:保存由 Recycler 分配的句柄
private final Recycler.Handle<PooledObject> handle;
// 你的业务字段
private int value;
public PooledObject(Recycler.Handle<PooledObject> handle) {
this.handle = handle;
}
public void setValue(int v) { this.value = v; }
public int getValue() { return value; }
// 回收时,务必把“自己”交还给句柄
public void recycle() {
handle.recycle(this);
}
@Override
public String toString() {
return "PooledObject{value=" + value + "}";
}
// 一个最小的 Recycler 子类:定义如何 new 出对象
private static final Recycler<PooledObject> RECYCLER =
new Recycler<PooledObject>() {
@Override
protected PooledObject newObject(Handle<PooledObject> handle) {
// 只在“池里拿不到”时 new;正常流程走复用
return new PooledObject(handle);
}
};
public static PooledObject acquire() {
return RECYCLER.get();
}
// Demo
public static void main(String[] args) {
// 第一次:可能触发 new
PooledObject a = PooledObject.acquire();
a.setValue(42);
System.out.println(a);
a.recycle(); // 放回池中
// 第二次:同线程再获取 → 走 Stack 快车道
PooledObject b = PooledObject.acquire();
System.out.println("reuse? " + (a == b));
b.recycle();
}
}
关键点回顾
-
newObject()
只在缺货时触发;正常都是从栈里弹。 -
recycle()
时,必须把“自己”传回句柄,并保证对象已重置到可再用状态(避免脏数据泄漏)。 -
如果你在另一个线程调用
recycle()
,Recycler 会自动走WeakOrderQueue
辅路,不需要你手工处理(但你要理解:这意味着回收并非立刻为创建者线程可见)。
1.8 一个贴近 Netty 的小例子:池化任务对象(类比 ByteBuf
的思路)
在 Netty 内部,池化不仅体现在 ByteBuf
上,也常用于频繁出现的微对象(如 Runnable
任务)。下面示例演示一个“池化的 Runnable”,在 EventLoop 上投递执行后回收:
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.Recycler;
import java.util.concurrent.TimeUnit;
public final class RecyclableTask implements Runnable {
private final Recycler.Handle<RecyclableTask> handle;
private Runnable actual; // 真实逻辑,或携带少量状态
private RecyclableTask(Recycler.Handle<RecyclableTask> handle) {
this.handle = handle;
}
private static final Recycler<RecyclableTask> RECYCLER =
new Recycler<RecyclableTask>() {
@Override
protected RecyclableTask newObject(Handle<RecyclableTask> handle) {
return new RecyclableTask(handle);
}
};
public static RecyclableTask acquire(Runnable actual) {
RecyclableTask t = RECYCLER.get();
t.actual = actual; // 复用对象,写入新的业务逻辑
return t;
}
@Override
public void run() {
try {
if (actual != null) actual.run();
} finally {
// 清理并回收
actual = null;
handle.recycle(this);
}
}
// demo
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup(1);
try {
for (int i = 0; i < 3; i++) {
group.next().schedule(
RecyclableTask.acquire(() -> System.out.println("tick " + System.nanoTime())),
100, TimeUnit.MILLISECONDS
);
}
Thread.sleep(500);
} finally {
group.shutdownGracefully();
}
}
}
启示
-
与
ByteBuf
的引用计数不同,Recycler 关注的是“对象壳的复用”。 -
你要保证任务执行完状态被清理,否则复用时串味。
-
如果任务跨线程被回收(比如被别的线程取消并回收),Recycler 会走
WeakOrderQueue
,不需要阻塞 EventLoop。
1.9 配置与可调参数(只点关键,详细放在后续章节)
-
回收比率:
io.netty.recycler.ratio
(默认 8),每 N 次尝试允许一次 push,用于缓慢增长池容量。该默认值在 4.1 xref 中可见。 -
最大容量默认值:
io.netty.recycler.maxCapacity.default
(以及可能的按类型自定义 key),用于限制每线程Stack
上可保留的句柄上限。该键在 4.0 源码注释处可见。 -
WeakOrderQueue.Link
容量:分段数组容量在源码里是固定常量,历史实现常见为 16,影响批转粒度与瞬时内存。 -
FastThreadLocal 相关:在
FastThreadLocalThread
上访问 ThreadLocal 变量更快(Recycler 及 Netty 内部大量使用),这是微优化拼图的一块。
提醒:不同 Netty 版本的默认值/属性名可能有差异;你在生产环境使用前,建议对照自家版本源码核对。
1.10 目标读者的“第一性原则”:什么时候该用 Recycler?
适用
-
高频、短命、易重置的小对象(任务、轻量状态包装、编解码临时容器)。
-
对延迟抖动敏感、对 Minor GC 频率有严格要求的 I/O 服务。
-
对象创建成本非零(含构造、数组分配、字段填充),且可以固定上限缓存。
不适用 / 慎用
-
重量级对象,或“重置成本 ≈ 新建成本”的对象。
-
生命周期复杂、跨线程不可控、很难保证“用完必回收”的对象。
-
容量不可预估或存在长尾闲置风险的对象(可能导致“囤积”内存)。
1.11 与 ByteBuf 池化的关系(为后文埋伏笔)
PooledByteBufAllocator
侧重“内存块(chunk/page)的池化与切分复用”,通过引用计数控制生命周期;而 Recycler 侧重“对象外壳”的复用(如 ByteBuf
的壳对象、内部辅助类、任务等)。两者经常协同出现:
-
ByteBuf
的内存从内存池复用(引用计数); -
ByteBuf
对象壳与其他微对象,从 Recycler 复用(Handle 机制);
从而实现“更少 GC(少分配) + 更少系统调用(少分配/释放直接内存)”。
第二章:Recycler 的核心数据结构(Stack、WeakOrderQueue)
Recycler
的高性能秘密在于其 分层缓存 设计:
-
本线程回收 → Stack(最快路径,零锁)
-
跨线程回收 → WeakOrderQueue(弱引用队列,延迟转移)
接下来我们逐一拆解。
2.1 Stack —— 线程本地对象池
Stack
是每个线程独享的对象池,内部用一个 数组 保存可复用对象。
核心思想:优先保证对象在本线程内的分配和回收,避免锁竞争。
核心属性
static final class Stack<T> {
final Recycler<T> parent; // 对应的 Recycler
final WeakReference<Thread> thread; // 归属线程
DefaultHandle<?>[] elements; // 存储可复用对象的数组
int size; // 当前栈内元素个数
...
}
-
elements
:存放DefaultHandle
,本质是对象引用容器。 -
thread
:用弱引用指向所属线程,便于垃圾回收。 -
size
:栈顶指针,典型的“数组 + 下标”结构。
核心操作
-
push(handle)
-
把回收对象放入栈顶。
-
如果是“跨线程回收”,不会直接放入 Stack,而是进入
WeakOrderQueue
。
-
-
pop()
-
从栈顶取对象,如果栈为空,尝试从
WeakOrderQueue
转移一批过来。 -
这是 线程本地复用 的关键。
-
👉 特点总结:
-
零锁操作,本线程独享。
-
O(1) push/pop。
-
需要和
WeakOrderQueue
配合,解决跨线程问题。
2.2 WeakOrderQueue —— 跨线程回收队列
当 A 线程分配对象,B 线程释放对象时,不能直接 push 到 A 的 Stack,否则就有并发问题。
解决办法:B 线程把对象放进 WeakOrderQueue,等待 A 线程下次分配时再批量转移。
核心属性
static final class WeakOrderQueue {
final WeakReference<Thread> owner; // 对应的 Stack 所属线程
final Link head; // 链表头
Link tail; // 链表尾
WeakOrderQueue next; // 链接多个队列
...
}
-
owner
:标记归属线程,如果线程已死,GC 可以清理。 -
head/tail
:内部是链表结构,存放Link
节点,每个Link
包含一批回收的对象。 -
next
:一个 Stack 可能对应多个 WeakOrderQueue(不同生产线程)。
内部结构 —— Link
static final class Link extends AtomicInteger {
final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
int writeIndex;
Link next;
}
-
elements
:固定容量数组(通常 16 个)。 -
writeIndex
:写指针,标记当前存放到哪。 -
next
:链表结构,方便扩展。
👉 WeakOrderQueue 就像“外来回收通道”,跨线程对象在这里暂存,等目标线程需要时再 批量转移 到 Stack。
2.3 Stack 与 WeakOrderQueue 的协作关系
可以用一条“流水线”来理解:
-
同线程回收
-
handle.recycle()
→ 直接 push 到Stack
→ O(1)
-
-
跨线程回收
-
handle.recycle()
→ 写入目标线程的WeakOrderQueue
-
下次目标线程调用
pop()
分配时 → 检查WeakOrderQueue
→ 批量转移到 Stack → 正常 pop
-
这种模式保证了:
-
无锁并发(跨线程不会直接抢占 Stack)。
-
缓存命中率高(同线程直接走本地栈)。
-
延迟回收(跨线程的对象批量转移,减少频繁操作)。
小结
-
Stack = 本线程高速缓存(数组栈,零锁,最快路径)。
-
WeakOrderQueue = 跨线程回收通道(链表队列,延迟批量转移)。
-
二者协作 = 保证对象在多线程环境下既安全,又高效地复用。
第三章:对象分配与回收的完整流程(同线程 / 异线程)
在理解了 Recycler
的核心数据结构 Stack
与 WeakOrderQueue
之后,我们可以进一步深入其对象分配与回收的完整流程。这一过程本质上就是 从线程本地池(Stack)中获取对象,或将对象归还到合适的数据结构中,从而最大限度地减少对象创建与 GC 压力。
3.1 对象分配流程(同线程)
当线程调用 Recycler.get()
获取对象时,大致会经历以下步骤:
-
定位 Stack
每个线程持有一个Stack
,Recycler
首先尝试从当前线程的Stack
中获取可复用对象。 -
检查 Stack 缓存
-
若
Stack
中存在空闲对象(通过elements
数组存储的DefaultHandle
),则直接弹出返回。 -
若为空,则进入
scavenge
流程,尝试从其他线程回收的对象(WeakOrderQueue)转移到本地。
-
-
创建新对象
如果仍然没有可用对象,则调用newObject()
方法创建新实例,并绑定一个DefaultHandle
用于生命周期管理。
👉 关键点:同线程分配对象时,路径非常短 —— 几乎就是 O(1)
的数组 pop 操作,因此性能极高。
3.2 对象回收流程(同线程)
当调用 handle.recycle()
时,如果是在对象创建线程中回收,则直接将对象推回到当前线程的 Stack
:
-
校验回收合法性
防止对象被错误地重复回收(通过recycleId
与lastRecycledId
标记)。 -
压回 Stack
将DefaultHandle
存放到Stack.elements
数组中,并更新索引。
👉 关键点:同线程回收不会涉及跨线程数据结构,回收路径与分配路径同样简洁高效。
3.3 对象回收流程(异线程)
异线程回收是 Recycler
设计的精妙之处。假设线程 A 创建对象,线程 B 回收对象,由于对象必须返回给线程 A 的 Stack
,但线程 B 不能直接操作 A 的 Stack(避免并发问题),于是设计了 WeakOrderQueue
。
流程如下:
-
线程 B 触发回收
当线程 B 调用handle.recycle()
时,检测到当前线程并非对象所属的线程。 -
查找或创建 WeakOrderQueue
-
每个线程回收时,会在目标 Stack 上维护一个
WeakOrderQueue
链表。 -
如果 B 第一次向 A 回收,会新建一个
WeakOrderQueue
并挂载到 A 的 Stack。
-
-
写入 WeakOrderQueue
-
对象的
DefaultHandle
被存入WeakOrderQueue
的Link
节点(类似单向队列)。 -
每个
Link
节点内部使用数组批量存储,避免频繁分配内存。
-
-
延迟转移
-
对象暂存在
WeakOrderQueue
中,并不会立即回到 A 的 Stack。 -
当线程 A 下次调用
get()
获取对象时,会触发scavenge()
,将 WeakOrderQueue 中的数据批量转移到本地 Stack。
-
👉 关键点:跨线程回收采用“延迟合并”策略,保证了无锁化与线程安全,代价是对象不能立即被复用,需要等原线程主动触发转移。
3.4 整体流程总结
可以用一个对比来直观理解:
-
同线程回收:对象 → 直接压回 Stack → 立即可复用。
-
异线程回收:对象 → 写入 WeakOrderQueue → 等待原线程
get()
时批量转移 → 才能复用。
这种设计既避免了锁竞争,又保证了对象最终归属原线程 Stack,从而维持了高效与线程安全的平衡。
3.5 Recycler 对象分配与回收流程图
1. 同线程分配 & 回收流程
[Thread-A 请求对象]
│
▼
[Recycler.get()]
│
├──> [Stack 中有可用对象?]
│ │
│ ├── 是 → [弹出 Stack 顶元素(DefaultHandle)]
│ │ │
│ │ └──> [返回对象给 Thread-A 使用]
│ │
│ └── 否 → [newObject() 创建新对象 + 绑定 DefaultHandle]
│ │
│ └──> [返回新对象给 Thread-A 使用]
│
▼
[Thread-A 使用完成 → 调用 handle.recycle(obj)]
│
└──> [直接 push 回 Thread-A 的 Stack]
│
└──> [下次 get() 可直接复用]
👉 特点:完全在同一个线程的 Stack
中完成,零锁开销,性能最佳。
2. 异线程分配 & 回收流程
[Thread-A 请求对象]
│
▼
[Recycler.get()]
│
└──> 与同线程流程一致(优先从 Stack 获取,否则 newObject)
│
▼
[Thread-A 使用完成 → 调用 handle.recycle(obj)]
│
▼
[当前线程 ≠ 对象绑定线程]
│
└──> [查找绑定线程的 Stack 对应的 WeakOrderQueue]
│
├── 已存在 → [将对象放入 WeakOrderQueue 中]
│
└── 不存在 → [创建新的 WeakOrderQueue → 注册到目标 Stack]
│
▼
[目标线程下次调用 get() 时]
│
└──> [若 Stack 为空 → 从 WeakOrderQueue 转移对象到 Stack]
│
└──> [返回对象供目标线程复用]
👉 特点:不同线程回收时,不会直接 push 到目标线程的 Stack,而是通过 WeakOrderQueue 做 延迟转移,保证无锁并发安全。
第四章:源码实现分析(Recycler、Stack、DefaultHandle)
在前两章我们已经知道,Recycler
的核心依赖 Stack(本线程对象池) 与 WeakOrderQueue(跨线程回收队列)。
下面通过源码解析,逐步拆解关键类与方法的实现。
1. Recycler 核心逻辑
Recycler
是对象池的入口,负责提供对象的获取与回收接口。
1.1 对象获取(get)
public final T get() {
Stack<T> stack = threadLocal.get();
DefaultHandle<T> handle = stack.pop();
if (handle == null) {
// 栈为空 → 创建新对象并包装为 DefaultHandle
handle = stack.newHandle(newObject(handle));
}
return (T) handle.value;
}
流程对应:
-
stack.pop()
→ 从本线程的Stack
弹出对象(同线程复用)。 -
handle == null
→ 栈为空,调用newObject()
创建新对象。 -
return handle.value
→ 返回对象给用户。
1.2 对象回收(recycle)
public final boolean recycle(T o, Handle<T> handle) {
((DefaultHandle<T>) handle).recycle(o);
return true;
}
这里 Recycler
只是一个入口,实际回收逻辑在 DefaultHandle.recycle() 中完成。
2. DefaultHandle 核心逻辑
DefaultHandle
是对象的“句柄”,保存了对象与其所属 Stack
的关系。
2.1 回收方法
public void recycle(Object object) {
if (recycleId == 0) {
stack.push(this);
} else {
// 跨线程回收 → WeakOrderQueue
stack.pushLater(this, Thread.currentThread());
}
}
关键点:
-
recycleId == 0
→ 说明对象在创建线程内回收,直接 push 到 Stack。 -
否则 → 调用
pushLater()
,将对象放入 WeakOrderQueue,等待目标线程消费。
3. Stack 核心逻辑
Stack
是对象池的本地容器,每个线程维护一个。
3.1 弹出对象
DefaultHandle<T> pop() {
if (size == 0) {
if (!scavenge()) {
return null;
}
}
size--;
return elements[size];
}
流程对应:
-
size == 0
→ 栈空 → 调用scavenge()
从WeakOrderQueue
转移对象。 -
转移失败仍然空 → 返回
null
,交由Recycler
创建新对象。
3.2 压入对象(同线程回收)
void push(DefaultHandle<?> item) {
elements[size++] = item;
}
同线程下非常简单:直接入栈,无锁操作。
3.3 跨线程回收(pushLater)
void pushLater(DefaultHandle<?> item, Thread thread) {
WeakOrderQueue queue = getOrCreateQueue(thread);
queue.add(item);
}
流程对应:
-
先找到当前线程对应的
WeakOrderQueue
。 -
queue.add(item)
将对象存入队列,供目标线程后续scavenge()
。
4. WeakOrderQueue 核心逻辑
用于跨线程回收的 延迟转移容器。
4.1 添加对象
void add(DefaultHandle<?> handle) {
if (tail == null || tail.isFull()) {
tail = new Link();
}
tail.elements[tail.writeIndex++] = handle;
}
对象会进入队列中的 Link
节点,形成链表结构。
4.2 转移对象
boolean transfer(Stack<?> dst) {
Link head = this.head;
if (head == null) return false;
// 把 WeakOrderQueue 中的对象搬运到目标线程的 Stack
DefaultHandle<?> handle = head.poll();
if (handle != null) {
dst.push(handle);
return true;
}
return false;
}
对应 Stack.pop()
→ scavenge()
的逻辑:当 Stack
空了,就会尝试从 WeakOrderQueue
搬运对象。
5. 源码对照流程表
场景 | 流程步骤 | 源码位置 |
---|---|---|
同线程 get() | 从 Stack.pop() 取对象 → 空则 newObject() | Recycler.get() / Stack.pop() |
同线程 recycle() | 直接 push 回 Stack | DefaultHandle.recycle() → Stack.push() |
异线程 recycle() | 放入 WeakOrderQueue | DefaultHandle.recycle() → Stack.pushLater() |
目标线程 get() | Stack 空 → scavenge() → WeakOrderQueue.transfer() | Stack.pop() / WeakOrderQueue.transfer() |
第五章:性能优化策略
Recycler
在 Netty 中作为高性能对象池,设计目标就是 在多线程场景下最大限度地减少锁竞争与内存分配开销。不过,开发者在使用和调优时仍需要结合业务特点,才能真正发挥其价值。以下从多个角度总结 性能优化策略:
5.1 控制池化对象的粒度
-
适合放入 Recycler 的对象
-
频繁创建/销毁的临时对象,例如
ByteBuf
、Promise
、EventExecutorTask
。 -
占用内存较小、生命周期短的对象。
-
-
不适合放入 Recycler 的对象
-
占用资源过大或涉及外部资源(文件句柄、Socket、DirectBuffer)。
-
生命周期复杂、需要跨线程频繁传递的对象。
-
👉 原则:小而轻的对象池化,避免因为维护复杂对象的生命周期导致 “对象池负担反而更重”。
5.2 合理配置最大容量(maxCapacityPerThread)
-
默认每个线程的
Stack
有一个上限maxCapacityPerThread
。 -
如果设置过小:频繁触发
newObject()
,降低复用率。 -
如果设置过大:内存压力增大,导致 GC 负担。
优化建议:
-
通过压测统计业务场景下的 峰值并发请求数,预估单线程需要缓存多少对象。
-
适度放大上限,避免对象池频繁丢弃/创建。
5.3 异线程回收的优化
异线程回收会走 WeakOrderQueue
,涉及更多步骤:
-
将对象放入队列(弱引用链表)。
-
等目标线程调用
get()
时再批量转移。
优化点:
-
减少跨线程回收:对象尽量在创建它的线程中使用并回收。
-
批量回收:在目标线程
get()
时,WeakOrderQueue 会 批量转移对象 到 Stack,降低迁移开销。
👉 实践技巧:如果能保证对象只在所属线程内使用,回收延迟会显著降低。
5.4 避免 GC 压力
Recycler 内部对象主要依赖 WeakOrderQueue
和 WeakReference
。
-
问题:当线程退出时,若
WeakOrderQueue
中仍有未转移的对象,会依赖 GC 才能释放,增加停顿。 -
优化建议:
-
在确定不再使用时,调用
Recycler.clear()
主动清理。 -
使用对象池时避免过大对象,否则 GC 频繁触发 Full GC,反而得不偿失。
-
5.5 与业务线程模型结合
-
在 EventLoop 模型 下(如 Netty NIO Reactor),对象池的效果最佳,因为对象几乎只在同一线程使用。
-
在 Worker 线程池模型 下(跨线程频繁传递),Recycler 的回收成本会变高。
👉 建议:明确线程模型,如果大量异线程传递对象,可以考虑 关闭 Recycler 或替换为其他对象池策略(如 JCTools MPSC 队列)。
5.6 实践经验总结
-
压测调优:通过压测观察
newObject()
调用频率,评估复用率。 -
对象池上限配置:结合实际内存和并发场景,避免盲目设大或设小。
-
尽量同线程回收:减少 WeakOrderQueue 使用,提升回收实时性。
-
避免池化过大对象:以防 GC 压力和内存泄漏。
-
结合 Netty 参数:如
io.netty.recycler.maxCapacityPerThread
系统属性进行动态调优。
第六章:典型使用场景
Recycler
作为 Netty 高性能对象复用的核心工具,其应用场景主要集中在 高频创建和销毁的轻量对象 上。下面从几个典型对象出发,分析 Recycler 的应用和优化意义。
6.1 PooledByteBuf(PooledDirectByteBuf / PooledHeapByteBuf)
6.1.1 场景背景
-
Netty 的
ByteBuf
是网络数据传输的核心对象,每次 IO 操作都会创建大量临时ByteBuf
。 -
频繁创建和 GC 对象会导致 堆外内存压力 或 DirectBuffer 内存泄漏。
6.1.2 Recycler 的应用
-
Netty 使用
Recycler
对PooledByteBuf
对象进行池化:public final class PooledByteBuf<T> extends AbstractByteBuf { private final Recycler.Handle<PooledByteBuf<T>> recyclerHandle; ... public void recycle() { recyclerHandle.recycle(this); } }
-
对象使用完成后,调用
recycle()
回收至 Recycler。 -
在同线程场景下,回收几乎是零开销,直接复用栈顶对象。
6.1.3 优势
-
避免堆外内存频繁分配和释放。
-
降低 GC 压力,提高 IO 性能。
-
利用同线程栈缓存,减少跨线程同步。
6.2 FastThreadLocalRunnable / EventExecutorTask
6.2.1 场景背景
-
Netty 的事件执行器(EventLoop)中,任务会频繁创建和提交。
-
每次创建 Runnable 或 PromiseTask 都可能导致短生命周期对象频繁分配。
6.2.2 Recycler 的应用
final class RunnableTask implements Runnable {
private final Recycler.Handle<RunnableTask> handle;
...
public void recycle() {
handle.recycle(this);
}
}
-
同样是通过
Recycler
复用 Runnable 对象,任务执行完毕后立即回收。
6.2.3 优势
-
避免线程池任务频繁分配,提升任务吞吐量。
-
对短生命周期、可复用任务非常适合。
6.3 Promise / Future 对象
6.3.1 场景背景
-
Netty 中每次异步操作(如写操作、连接操作)都会创建
Promise
或Future
对象。 -
高并发场景下,这些对象数量非常多,容易触发 GC。
6.3.2 Recycler 的应用
public final class DefaultPromise<V> extends DefaultFuture<V> {
private final Recycler.Handle<DefaultPromise<V>> handle;
...
public void recycle() {
handle.recycle(this);
}
}
-
Promise 对象生命周期短,完成后立即回收。
6.3.3 优势
-
避免大量短生命周期对象增加 GC 压力。
-
高效支持异步 IO 高并发场景。
6.4 适用场景总结
场景对象 | 生命周期特点 | Recycler 价值 |
---|---|---|
PooledByteBuf | 短小且频繁分配 | 降低堆外内存分配,提升性能 |
FastThreadLocalRunnable | 短任务频繁创建 | 减少 Runnable 对象开销 |
Promise / Future | 短生命周期 | 降低高并发下 GC 压力 |
其他轻量对象(用户自定义) | 短生命周期、高频 | 避免对象重复创建,提高吞吐量 |
6.5 使用经验
-
对象尽量同线程使用
-
保证最大复用率,减少 WeakOrderQueue 迁移开销。
-
-
对象池容量需合理配置
-
针对高并发 IO 场景,可适度增加 Stack 大小,避免频繁创建对象。
-
-
注意生命周期与资源管理
-
池化对象尽量不持有大资源,如文件句柄、Socket,否则可能导致内存泄漏或资源未释放。
-
✅ 总结:
Recycler 在 Netty 内部主要用于 短生命周期、高频对象的复用,通过 Stack + WeakOrderQueue 机制,实现了 同线程零开销复用 + 跨线程延迟复用,有效减少 GC 压力,提高 IO 吞吐量。
第七章:常见问题与最佳实践
虽然 Netty 的 Recycler
设计高效,但在实际应用中仍有一些常见问题需要开发者注意。掌握这些问题与对应的实践技巧,可以避免性能下降、内存泄漏或线程安全隐患。
7.1 常见问题
7.1.1 对象被重复回收
-
问题描述:同一个对象被多次调用
recycle()
,可能破坏栈或队列的状态。 -
原因:
DefaultHandle
中有recycleId
与lastRecycledId
控制回收状态,但如果手动操作不当,仍可能重复回收。 -
表现:Stack 中出现相同对象,导致同一对象被多次使用,可能引发 并发修改异常 或逻辑错误。
示例:
PooledObject obj = recycler.get();
obj.recycle();
obj.recycle(); // 错误示范:重复回收
解决方案:
-
只通过对象本身的
recycle()
方法回收一次。 -
避免将对象传递到其他线程重复回收。
7.1.2 异线程回收导致延迟复用
-
问题描述:跨线程回收时,对象不会立即回到目标线程的 Stack,而是通过
WeakOrderQueue
延迟转移。 -
表现:短时间内对象复用率低,可能触发频繁的
newObject()
创建新对象。
优化策略:
-
尽量保持对象的创建与回收在同一线程。
-
对跨线程回收的对象,可以考虑批量操作,减少 WeakOrderQueue 的写入频率。
7.1.3 弱引用队列可能导致内存滞留
-
问题描述:
WeakOrderQueue
内部使用弱引用指向线程,当线程退出但队列未被消费时,对象会滞留等待 GC。 -
表现:短时间内占用堆内存或直接内存(DirectBuffer),可能触发 Full GC。
解决方案:
-
在线程退出前,主动清理对象池(调用
Recycler.clear()
或释放资源)。 -
对象池中避免存放大对象或长生命周期对象。
7.1.4 对象池容量不合理
-
问题描述:
Stack
容量过小 → 频繁创建新对象;容量过大 → 内存占用增加。 -
表现:高并发场景下性能下降或堆内存压力过大。
优化策略:
-
根据业务压力进行压测,确定合适的
maxCapacityPerThread
。 -
对不同对象类型可分别配置不同的池化策略。
7.1.5 GC 压力与 DirectBuffer 泄漏
-
问题描述:Recycler 用于池化 Direct 内存对象时,如果回收路径不及时,会增加 GC 压力。
-
表现:Full GC 频繁、Native 内存占用持续增长。
优化策略:
-
同线程回收 Direct 内存对象,减少 WeakOrderQueue 异步转移。
-
在高并发场景下,考虑限制池化对象大小或数量,避免占用过多 Direct 内存。
7.2 最佳实践
7.2.1 优先同线程复用
-
Recycler 设计的高性能关键在于 同线程的 Stack 零锁复用。
-
对象尽量在创建线程内回收,降低 WeakOrderQueue 的使用。
7.2.2 合理设置对象池容量
-
使用系统属性或构造参数调节
maxCapacityPerThread
。 -
针对短生命周期、高频使用对象,如
ByteBuf
或任务对象,可以适当放大容量。 -
对大对象或低频对象,考虑关闭池化,避免占用内存。
7.2.3 避免重复回收
-
每个对象只允许回收一次。
-
不要手动干预 Recycler 内部状态或跨线程回收的队列。
7.2.4 控制跨线程回收量
-
对跨线程传递的对象,考虑批量回收或延迟回收策略,减少 WeakOrderQueue 写入开销。
7.2.5 监控对象池使用情况
-
可在开发或压测阶段打印
Stack
和WeakOrderQueue
的大小与对象创建次数。 -
及时发现复用率低、对象创建频繁或内存滞留问题,进行调优。
7.3 小结
-
Recycler 在高性能场景下非常高效,但 使用不当容易导致性能下降或内存问题。
-
核心原则:
-
优先同线程回收。
-
合理配置容量。
-
避免重复回收。
-
控制跨线程回收频率。
-
监控复用率与对象创建次数。
-
-
掌握这些实践经验,可以让 Recycler 在 Netty 或其他高并发应用中发挥 最优性能。