sync.Pool对象在GC时被运行时批量丢弃而非逐个销毁,其victim cache机制使每次GC清空上轮本地池,shared链表仅保留最后一次Put的对象。

sync.Pool 的对象不是“被销毁”,而是被 runtime 批量丢弃
GC 触发时,sync.Pool 中未被引用的对象不会逐个调用析构逻辑(它根本没有析构机制),而是由运行时直接切断所有池内指针引用——相当于把整个本地池的链表头置为 nil。这不是对象自身的销毁,是池子主动“放弃持有”。所以你看到两次 GC 之间对象消失,不是 GC 清了它,是 Pool 在上一次 GC 后的 victim 阶段已清空本地缓存,而 global 部分只保留了最后一次 Put() 的那个对象。
victim cache 机制:每次 GC 都会清掉上一轮的本地池
Go 运行时为每个 P 维护两级缓存:private(仅当前 goroutine 可用)和 shared(跨 goroutine 共享)。但关键在 victim:每次 GC 开始前,当前所有 P 的本地池会被“降级”为 victim cache;GC 完成后,victim 被彻底清空,shared 链表也只保留最近一次 Put() 的节点。这意味着:
- 哪怕你刚
Put()一个对象,只要它没被任何 goroutine 持有,下一次 GC 就可能让它从池中彻底消失 - 没有 API 能查某个对象是否还在池里,
pprof -alloc_space或runtime.ReadMemStats中的sync.Pool相关指标才是真实依据 - 频繁阻塞(如
net.Conn.Read)会导致 goroutine 迁移 P,原 P 的 private 对象进 victim 后直接丢,复用率断崖下跌
为什么 New 函数不保证只调一次,却总看到对象“突然没了”?
New 是兜底工厂,不是生命周期管理器。它只在 Get() 发现本地 + shared 都为空时才触发,且可能被多个 goroutine 并发调用——这本身就会导致重复构造。更关键的是:New 返回的对象一旦被 Get() 拿走,就脱离了 Pool 管理;如果使用者没及时 Put() 回去,或 Put() 前忘了 Reset(),那这个对象下次 Get() 时大概率是脏的,或者干脆因 GC 被丢弃后由 New 新建一个。
- 不要在
New里初始化全局状态(比如复用同一个*rand.Rand) - 不要依赖
New做资源分配(如打开文件、建立连接),它不控制对象存活时间 -
New必须返回全新实例,例如&bytes.Buffer{},而不是&globalBuf
真正影响对象“存活”的,是 goroutine 是否持续绑定到同一个 P
对象是否能留在池里,不取决于你调了多少次 Put(),而取决于 goroutine 的调度稳定性。固定在某个 P 上的 goroutine(比如纯计算型、无系统调用),Get()/Put() 基本走 fast path,对象可长期复用;但一旦发生系统调用返回、抢占或手动生成新 goroutine(如 go fn()),就可能换 P,原 P 的 private 对象进 victim 后清空,shared 部分又只留一个,等于池子“半残”。
- 高并发 HTTP 服务中,大量
io.Copy或json.Unmarshal容易触发调度迁移,此时sync.Pool效果可能不如预分配切片 - 压测时别只看 QPS,重点观察
runtime.mallocgc占比和sync.Pool.allocsprofile,数据比直觉可靠 - 如果你的应用 goroutine 生命周期短于一次 GC 周期(比如平均 20ms),那
sync.Pool大概率帮不上忙










