mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1223 字
3 分钟
对象池(Object Pool)
2026-06-13

一、为什么需要对象池#

想象一个游戏引擎每帧要创建上千个粒子对象。粒子诞生、飞行、消亡,下一帧又是新一轮粒子。每一次 new Particle() 都意味着内存分配,每一次消亡都等着 GC 来回收。GC 一旦触发,整个游戏循环停顿几毫秒,玩家感受到的就是画面卡顿。

再比如一个 HTTP 服务器,每个请求都需要一个 bytes.Buffer 来拼装响应。QPS 过万时,每秒分配上万个 Buffer,GC 压力陡增。服务明明还有足够的 CPU 处理请求,却被垃圾回收拖慢了节奏。

问题的根源在于:频繁创建和销毁同类型的短期对象,会让分配器和 GC 都过载。创建需要找空闲内存块、初始化字段;销毁需要标记为可回收、等待 GC 扫描。如果这些对象的结构都一样,为什么要反复经历创建-销毁的轮回?

对象池的思路很简单:提前造好一批对象,用的时候取,用完还回去,不销毁

二、现实类比#

共享单车站点。不用每次出行都买一辆新车,你从停靠点取一辆,骑完还回去。车是预先购置的,被很多人反复使用。站点里总有一定数量的车待命,高峰期不够了再临时调配。

三、核心思想#

对象池维护一组预初始化的对象。调用方通过 Get() 获取对象,用完通过 Put() 归还而非丢弃。归还时重置对象状态,使其像新的一样等待下一次使用。

sequenceDiagram participant C as 调用方 participant P as 对象池 C->>P: Get() P-->>C: 返回已有对象(或创建新的) Note over C: 使用对象... C->>P: Put(对象) Note over P: 重置状态,存入池中 Note over P: 下次 Get() 无需分配

核心权衡:内存占用(空闲对象常驻池中)vs CPU/GC 节省(热路径零分配)。

操作复杂度说明
Get(池有空闲)O(1)直接返回已有对象
Get(池为空)O(alloc)创建新对象
Put(归还)O(1)重置状态 + 推入空闲列表
内存开销O(池大小)空闲对象保留在储备中

四、变体与对比#

模式与对象池的关系适用场景
空闲链表(Free List)空闲链表管理池内部的槽位分配固定大小内存块的分配回收
Arena 分配器Arena 为池对象批量分配内存同一生命周期的大量临时对象
引用计数引用计数追踪何时可将对象归还池中共享所有权的资源管理
信号量池大小充当信号量限制并发使用连接池、线程池等有界资源

五、多语言实现#

5.1 Go 实现#

Go 标准库的 sync.Pool 是生产级对象池,采用每 P 本地池实现无锁访问。下面演示一个通用的对象池:

package pool
import "sync"
// BufferPool 复用 bytes.Buffer,减少 HTTP 处理中的分配
var bufPool = sync.Pool{
New: func() any {
// 预分配 4KB 容量
b := make([]byte, 0, 4096)
return &b
},
}
// GetBuffer 从池中获取一个 buffer
func GetBuffer() *[]byte {
return bufPool.Get().(*[]byte)
}
// PutBuffer 归还 buffer 到池中
func PutBuffer(buf *[]byte) {
// 重置长度但保留容量,下次可直接复用
*buf = (*buf)[:0]
bufPool.Put(buf)
}
// ProcessRequest 演示池化 buffer 的典型用法
func ProcessRequest(data []byte) []byte {
buf := GetBuffer()
defer PutBuffer(buf)
*buf = append(*buf, data...)
// ... 业务处理 ...
// 必须复制结果,因为 buf 会被归还复用
result := make([]byte, len(*buf))
copy(result, *buf)
return result
}

关键点:sync.PoolGet() 先从当前 P 的本地池取(无锁),本地池空了再去其他 P 偷。Put() 也优先放回本地池。这种分片设计极大减少了锁竞争。

5.2 TypeScript 实现#

class ObjectPool<T> {
private pool: T[] = [];
private factory: () => T;
private reset: (obj: T) => void;
constructor(
factory: () => T,
reset: (obj: T) => void,
initialSize = 0
) {
this.factory = factory;
this.reset = reset;
// 预热:提前创建初始对象
for (let i = 0; i < initialSize; i++) {
this.pool.push(factory());
}
}
get(): T {
// 池中有空闲对象则复用,否则新建
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return this.factory();
}
release(obj: T): void {
// 重置对象状态后放回池中
this.reset(obj);
this.pool.push(obj);
}
get size(): number {
return this.pool.length;
}
}
// 使用示例:连接池
interface Connection {
id: number;
query(sql: string): string;
reset(): void;
}
const connPool = new ObjectPool<Connection>(
() => ({ id: Math.random(), query: (sql) => `result: ${sql}`, reset() {} }),
(conn) => conn.reset(),
5
);

六、生产验证#

项目源码位置用途
Go 标准库pool.go#L52-L97sync.PoolGet() 先查 per-P 本地池(无锁),回退到从其他 P 偷取。被 fmtencoding/json、HTTP 处理器广泛使用
Godot 引擎pooled_list.h#L35-L100基于 freelist 的对象池,元素在连续页中分配,避免每帧为实体、粒子、物理体分配内存
.NET 运行时ArrayPool.csArrayPool<T> 提供可复用数组的共享池,被 ASP.NET Core 和 System.Text.Json 使用

七、小结#

何时使用:

  • 高频分配场景——游戏循环、请求处理、粒子系统
  • 构造昂贵的对象——数据库连接、线程上下文、大缓冲区
  • GC 敏感的系统——实时服务、游戏引擎、低延迟交易
  • 固定资源限制——连接池、线程池、文件描述符池

何时不用:

  • 廉价对象——分配快且 GC 压力不大时,池增加了不必要的复杂性
  • 生命周期差异大——对象持有时间不可预测时,池帮不上忙
  • 小规模使用——对象数量少时,池的管理开销超过节省
  • 不可变对象——池只对需要重置的可变对象有意义

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

对象池(Object Pool)
https://blog.souloss.com/posts/programming/memory/memory-object-pool/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时