Go sync.Pool 的垃圾回收解读

70 阅读2分钟

sync.Pool 是 Go 提供一个对象池,用于缓存临时对象以减少垃圾回收压力。那么对象池中的对象会被垃圾回收吗,它是如何减轻 gc 回收压力的呢? 问题的一切答案都需要从 sync.pool的源码开始说起:

// A Pool is a set of temporary objects that may be individually saved and
// retrieved.
//
// Any item stored in the Pool may be removed automatically at any time without
// notification. If the Pool holds the only reference when this happens, the
// item might be deallocated.
//
// A Pool is safe for use by multiple goroutines simultaneously.
//
// Pool's purpose is to cache allocated but unused items for later reuse,
// relieving pressure on the garbage collector. That is, it makes it easy to
// build efficient, thread-safe free lists. However, it is not suitable for all
// free lists.
//
// An appropriate use of a Pool is to manage a group of temporary items
// silently shared among and potentially reused by concurrent independent
// clients of a package. Pool provides a way to amortize allocation overhead
// across many clients.
//
// An example of good use of a Pool is in the fmt package, which maintains a
// dynamically-sized store of temporary output buffers. The store scales under
// load (when many goroutines are actively printing) and shrinks when
// quiescent.
//
// On the other hand, a free list maintained as part of a short-lived object is
// not a suitable use for a Pool, since the overhead does not amortize well in
// that scenario. It is more efficient to have such objects implement their own
// free list.
//
// A Pool must not be copied after first use.
//
// In the terminology of [the Go memory model], a call to Put(x) “synchronizes before”
// a call to [Pool.Get] returning that same value x.
// Similarly, a call to New returning x “synchronizes before”
// a call to Get returning that same value x.
//
// [the Go memory model]: https://go.dev/ref/mem
type Pool struct {
	noCopy noCopy

	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array

	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array

	// New optionally specifies a function to generate
	// a value when Get would otherwise return nil.
	// It may not be changed concurrently with calls to Get.
	New func() any
}

数据结构描述了 sync.Pool的几个特点:

  1. pool 是存储了一些对象的合集
  2. pool 中的对象会被垃圾回收
  3. 并发安全
  4. 适用于短生命周期的对象
  5. 不能被复制

因此就回答了一开始的问题,pool 中的对象是会被垃圾回收的。 那么 pool 中的对象是如何被垃圾回收的呢? 关键在于数据结构中的这四个变量

local unsafe.Pointer // 当前缓存对象数据 
localSize uintptr // local数组大小
victim unsafe.Pointer // 上一轮的缓存对象数组
victimSize uintptr // victim数组大小

通过使用分代缓存策略,将上一代对象池p.victim置位 nil 让 gc 回收,把当前对象池p.local 更迭到老对象池.以达到 gc 性能更加平滑。具体的 gc 算法是源码中的 poolCleanup()

func poolCleanup() {
    // 这个函数在STW(Stop The World)期间被调用
    
    // 1. 清空所有victim缓存
    for _, p := range oldPools {
        p.victim = nil
        p.victimSize = 0
    }
    
    // 2. 将primary缓存降级为victim缓存
    for _, p := range allPools {
        p.victim = p.local      // primary -> victim
        p.victimSize = p.localSize
        p.local = nil           // 清空primary
        p.localSize = 0
    }
    
    // 3. 更新池的分类
    oldPools, allPools = allPools, nil
}