finalizer 是与对象关联的一个函数,通过 runtime.SetFinalizer
来设置,它在对象被 GC 的时候,这个 finalizer 会被调用,以完成对象生命中最后一程。由于 finalizer 的存在,导致了对象在三色标记中,不可能被标为白色对象,也就是垃圾,所以,这个对象的生命也会得以延续一个 GC 周期。正如 defer 一样,我们也可以通过 Finalizer 完成一些类似于资源释放的操作
1. 结构概览
1.1. heap
type mspan struct {
// 当前 span 上所有对象的 special 串成链表
// special 中有个 offset,就是数据对象在 span 上的 offset,通过 offset,将数据对象和 special 关联起来
specials *special // linked list of special records sorted by offset.
}
1.2. special
type special struct {
next *special // linked list in span
// 数据对象在 span 上的 offset
offset uint16 // span offset of object
kind byte // kind of special
}
1.3. specialfinalizer
type specialfinalizer struct {
special special
fn *funcval // May be a heap pointer.
// return 的数据的大小
nret uintptr
// 第一个参数的类型
fint *_type // May be a heap pointer, but always live.
// 与 finalizer 关联的数据对象的指针类型
ot *ptrtype // May be a heap pointer, but always live.
}
1.4. finalizer
type finalizer struct {fn *funcval // function to call (may be a heap pointer)
arg unsafe.Pointer // ptr to object (may be a heap pointer)
nret uintptr // bytes of return values from fn
fint *_type // type of first argument of fn
ot *ptrtype // type of ptr to object (may be a heap pointer)
}
1.5. 全局变量
var finlock mutex // protects the following variables
// 运行 finalizer 的 g,只有一个 g,不用的时候休眠,需要的时候再唤醒
var fing *g // goroutine that runs finalizers
// finalizer 的全局队列,这里是已经设置的 finalizer 串成的链表
var finq *finblock // list of finalizers that are to be executed
// 已经释放的 finblock 的链表,用 finc 缓存起来,以后需要使用的时候可以直接取走,避免再走一遍内存分配了
var finc *finblock // cache of free blocks
var finptrmask [_FinBlockSize / sys.PtrSize / 8]byte
var fingwait bool // fing 的标志位,通过 fingwait 和 fingwake,来确定是否需要唤醒 fing
var fingwake bool
// 所有的 blocks 串成的链表
var allfin *finblock // list of all blocks
2. 源码分析
2.1. 创建 finalizer
2.1.1. main
func main() {
// i 就是后面说的 数据对象
var i = 3
// 这里的 func 就是后面一直说的 finalizer
runtime.SetFinalizer(&i, func(i *int) {fmt.Println(i, *i, "set finalizer")
})
time.Sleep(time.Second * 5)
}
2.1.2. SetFinalizer
根据 数据对象,生成一个 special 对象,并绑定到 数据对象 所在的 span,串联到 span.specials 上,并且确保 fing 的存在
func SetFinalizer(obj interface{}, finalizer interface{}) {
if debug.sbrk != 0 {
// debug.sbrk never frees memory, so no finalizers run
// (and we don't have the data structures to record them).
return
}
e := efaceOf(&obj)
etyp := e._type
// ---- 省略数据校验的逻辑 ---
ot := (*ptrtype)(unsafe.Pointer(etyp))
// find the containing object
// 在内存中找不到分配的地址时 base==0,setFinalizer 是在内存回收的时候调用,没有分配就不会回收
base, _, _ := findObject(uintptr(e.data), 0, 0)
f := efaceOf(&finalizer)
ftyp := f._type
// 如果 finalizer type == nil,尝试移除(没有的话,就不需要移除了)if ftyp == nil {
// switch to system stack and remove finalizer
systemstack(func() {removefinalizer(e.data)
})
return
}
// --- 对 finalizer 参数数量及类型进行校验 --
if ftyp.kind&kindMask != kindFunc {throw("runtime.SetFinalizer: second argument is" + ftyp.string() + ", not a function")
}
ft := (*functype)(unsafe.Pointer(ftyp))
if ft.dotdotdot() {throw("runtime.SetFinalizer: cannot pass" + etyp.string() + "to finalizer" + ftyp.string() + "because dotdotdot")
}
if ft.inCount != 1 {throw("runtime.SetFinalizer: cannot pass" + etyp.string() + "to finalizer" + ftyp.string())
}
fint := ft.in()[0]
switch {
case fint == etyp:
// ok - same type
goto okarg
case fint.kind&kindMask == kindPtr:
if (fint.uncommon() == nil || etyp.uncommon() == nil) && (*ptrtype)(unsafe.Pointer(fint)).elem == ot.elem {
// ok - not same type, but both pointers,
// one or the other is unnamed, and same element type, so assignable.
goto okarg
}
case fint.kind&kindMask == kindInterface:
ityp := (*interfacetype)(unsafe.Pointer(fint))
if len(ityp.mhdr) == 0 {
// ok - satisfies empty interface
goto okarg
}
if _, ok := assertE2I2(ityp, *efaceOf(&obj)); ok {goto okarg}
}
throw("runtime.SetFinalizer: cannot pass" + etyp.string() + "to finalizer" + ftyp.string())
okarg:
// compute size needed for return parameters
// 计算返回参数的大小并进行对齐
nret := uintptr(0)
for _, t := range ft.out() {nret = round(nret, uintptr(t.align)) + uintptr(t.size)
}
nret = round(nret, sys.PtrSize)
// make sure we have a finalizer goroutine
// 确保 finalizer 有一个 goroutine
createfing()
systemstack(func() {
// 却换到 g0,添加 finalizer,并且不能重复设置
if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {throw("runtime.SetFinalizer: finalizer already set")
}
})
}
这里逻辑没什么复杂的,只是在参数、类型的判断等上面,比较的麻烦
2.1.3. removefinalizer
通过 removespecial,找到数据对象 p 所对应的 special 对象,如果找到的话,释放 mheap 上对应的内存
func removefinalizer(p unsafe.Pointer) {
// 根据数据 p 找到对应的 special 对象
s := (*specialfinalizer)(unsafe.Pointer(removespecial(p, _KindSpecialFinalizer)))
if s == nil {return // there wasn't a finalizer to remove}
lock(&mheap_.speciallock)
// 释放找到的 special 所对应的内存
mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
unlock(&mheap_.speciallock)
}
这里的函数,虽然叫 removefinalizer,但是这里暂时跟 finalizer 结构体没有关系,都是在跟 special 结构体打交道,后面的 addfinalizer 也是一样的
2.1.4. removespecial
遍历数据所在的 span 的 specials,如果找到了指定数据 p 的 special 的话,就从 specials 中移除,并返回
func removespecial(p unsafe.Pointer, kind uint8) *special {
// 找到数据 p 所在的 span
span := spanOfHeap(uintptr(p))
if span == nil {throw("removespecial on invalid pointer")
}
// Ensure that the span is swept.
// Sweeping accesses the specials list w/o locks, so we have
// to synchronize with it. And it's just much safer.
mp := acquirem()
// 保证 span 被清扫过了
span.ensureSwept()
// 获取数据 p 的偏移量,根据偏移量去寻找 p 对应的 special
offset := uintptr(p) - span.base()
lock(&span.speciallock)
t := &span.specials
// 遍历 span.specials 这个链表
for {
s := *t
if s == nil {break}
// This function is used for finalizers only, so we don't check for
// "interior" specials (p must be exactly equal to s->offset).
if offset == uintptr(s.offset) && kind == s.kind {
// 找到了,修改指针,将当前找到的 special 移除
*t = s.next
unlock(&span.speciallock)
releasem(mp)
return s
}
t = &s.next
}
unlock(&span.speciallock)
releasem(mp)
// 没有找到,就返回 nil
return nil
}
2.1.5. addfinalizer
正好跟 removefinalizer 相反,这个就是根据数据对象 p,创建对应的 special,然后添加到 span.specials 链表上面
func addfinalizer(p unsafe.Pointer, f *funcval, nret uintptr, fint *_type, ot *ptrtype) bool {lock(&mheap_.speciallock)
// 分配出来一块内存供 finalizer 使用
s := (*specialfinalizer)(mheap_.specialfinalizeralloc.alloc())
unlock(&mheap_.speciallock)
s.special.kind = _KindSpecialFinalizer
s.fn = f
s.nret = nret
s.fint = fint
s.ot = ot
if addspecial(p, &s.special) {return true}
// There was an old finalizer
// 没有添加成功,是因为 p 已经有了一个 special 对象了
lock(&mheap_.speciallock)
mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
unlock(&mheap_.speciallock)
return false
}
2.1.6. addspecial
这里是添加 special 的主逻辑
func addspecial(p unsafe.Pointer, s *special) bool {span := spanOfHeap(uintptr(p))
if span == nil {throw("addspecial on invalid pointer")
}
// 同 removerspecial 一样,确保这个 span 已经清扫过了
mp := acquirem()
span.ensureSwept()
offset := uintptr(p) - span.base()
kind := s.kind
lock(&span.speciallock)
// Find splice point, check for existing record.
t := &span.specials
for {
x := *t
if x == nil {break}
if offset == uintptr(x.offset) && kind == x.kind {
// 已经存在了,不能在增加了,一个数据对象,只能绑定一个 finalizer
unlock(&span.speciallock)
releasem(mp)
return false // already exists
}
if offset < uintptr(x.offset) || (offset == uintptr(x.offset) && kind < x.kind) {break}
t = &x.next
}
// Splice in record, fill in offset.
// 添加到 specials 队列尾
s.offset = uint16(offset)
s.next = *t
*t = s
unlock(&span.speciallock)
releasem(mp)
return true
}
2.1.7. createfing
这个函数是保证,创建了 finalizer 之后,有一个 goroutine 去运行,这里只运行一次,这个 goroutine 会由全局变量 fing 记录
func createfing() {
// start the finalizer goroutine exactly once
// 进创建一个 goroutine,进行时刻监控运行
if fingCreate == 0 && atomic.Cas(&fingCreate, 0, 1) {
// 开启一个 goroutine 运行
go runfinq()}
}
2.2. 执行 finalizer
在上面的 createfing
的会尝试创建一个 goroutine 去执行,接下来就分析一下执行流程吧
func runfinq() {
var (
frame unsafe.Pointer
framecap uintptr
)
for {lock(&finlock)
// 获取 finq 全局队列,并清空全局队列
fb := finq
finq = nil
if fb == nil {
// 如果全局队列为空,休眠当前 g,等待被唤醒
gp := getg()
fing = gp
// 设置 fing 的状态标志位
fingwait = true
goparkunlock(&finlock, waitReasonFinalizerWait, traceEvGoBlock, 1)
continue
}
unlock(&finlock)
// 循环执行 runq 链表里的 fin 数组
for fb != nil {
for i := fb.cnt; i > 0; i-- {f := &fb.fin[i-1]
// 获取存储当前 finalizer 的返回数据的大小,如果比之前大,则分配
framesz := unsafe.Sizeof((interface{})(nil)) + f.nret
if framecap < framesz {
// The frame does not contain pointers interesting for GC,
// all not yet finalized objects are stored in finq.
// If we do not mark it as FlagNoScan,
// the last finalized object is not collected.
frame = mallocgc(framesz, nil, true)
framecap = framesz
}
if f.fint == nil {throw("missing type in runfinq")
}
// frame is effectively uninitialized
// memory. That means we have to clear
// it before writing to it to avoid
// confusing the write barrier.
// 清空 frame 内存存储
*(*[2]uintptr)(frame) = [2]uintptr{}
switch f.fint.kind & kindMask {
case kindPtr:
// direct use of pointer
*(*unsafe.Pointer)(frame) = f.arg
case kindInterface:
ityp := (*interfacetype)(unsafe.Pointer(f.fint))
// set up with empty interface
(*eface)(frame)._type = &f.ot.typ
(*eface)(frame).data = f.arg
if len(ityp.mhdr) != 0 {
// convert to interface with methods
// this conversion is guaranteed to succeed - we checked in SetFinalizer
*(*iface)(frame) = assertE2I(ityp, *(*eface)(frame))
}
default:
throw("bad kind in runfinq")
}
// 调用 finalizer 函数
fingRunning = true
reflectcall(nil, unsafe.Pointer(f.fn), frame, uint32(framesz), uint32(framesz))
fingRunning = false
// Drop finalizer queue heap references
// before hiding them from markroot.
// This also ensures these will be
// clear if we reuse the finalizer.
// 清空 finalizer 的属性
f.fn = nil
f.arg = nil
f.ot = nil
atomic.Store(&fb.cnt, i-1)
}
// 将已经完成的 finalizer 放入 finc 以作缓存,避免再次分配内存
next := fb.next
lock(&finlock)
fb.next = finc
finc = fb
unlock(&finlock)
fb = next
}
}
}
看完上面的流程的时候,突然发现有点懵逼
- 全局队列 finq 中是什么时候被插入数据 finalizer 的?
- g 如果休眠了,那怎么被唤醒呢?
先针对第一个问题分析:
插入队列的操作,要追溯到我们之前分析的 GC 深入理解 Go- 垃圾回收机制 了,在 sweep
中有下面一段函数
2.2.1. sweep
func (s *mspan) sweep(preserve bool) bool {
....
specialp := &s.specials
special := *specialp
for special != nil {
....
if special.kind == _KindSpecialFinalizer || !hasFin {
// Splice out special record.
y := special
special = special.next
*specialp = special
// 加入全局 finq 队列的入口就在这里了
freespecial(y, unsafe.Pointer(p), size)
}
....
}
....
}
2.2.2. freespecial
在 gc 的时候,不仅要把 special 对应的内存释放掉,而且把 specials 整理创建对应 dinalizer 对象,并插入到 finq 队列里面
func freespecial(s *special, p unsafe.Pointer, size uintptr) {
switch s.kind {
case _KindSpecialFinalizer:
// 把这个 finalizer 加入到全局队列
sf := (*specialfinalizer)(unsafe.Pointer(s))
queuefinalizer(p, sf.fn, sf.nret, sf.fint, sf.ot)
lock(&mheap_.speciallock)
mheap_.specialfinalizeralloc.free(unsafe.Pointer(sf))
unlock(&mheap_.speciallock)
// 下面两种情况不在分析范围内,省略
case _KindSpecialProfile:
sp := (*specialprofile)(unsafe.Pointer(s))
mProf_Free(sp.b, size)
lock(&mheap_.speciallock)
mheap_.specialprofilealloc.free(unsafe.Pointer(sp))
unlock(&mheap_.speciallock)
default:
throw("bad special kind")
panic("not reached")
}
}
2.2.3. queuefinalizer
func queuefinalizer(p unsafe.Pointer, fn *funcval, nret uintptr, fint *_type, ot *ptrtype) {lock(&finlock)
// 如果 finq 为空或 finq 的内部数组已经满了,则从 finc 或重新分配 来获取 block 并插入到 finq 的链表头
if finq == nil || finq.cnt == uint32(len(finq.fin)) {
if finc == nil {finc = (*finblock)(persistentalloc(_FinBlockSize, 0, &memstats.gc_sys))
finc.alllink = allfin
allfin = finc
if finptrmask[0] == 0 {
// Build pointer mask for Finalizer array in block.
// Check assumptions made in finalizer1 array above.
if (unsafe.Sizeof(finalizer{}) != 5*sys.PtrSize ||
unsafe.Offsetof(finalizer{}.fn) != 0 ||
unsafe.Offsetof(finalizer{}.arg) != sys.PtrSize ||
unsafe.Offsetof(finalizer{}.nret) != 2*sys.PtrSize ||
unsafe.Offsetof(finalizer{}.fint) != 3*sys.PtrSize ||
unsafe.Offsetof(finalizer{}.ot) != 4*sys.PtrSize) {throw("finalizer out of sync")
}
for i := range finptrmask {finptrmask[i] = finalizer1[i%len(finalizer1)]
}
}
}
// 从 finc 中移除并获取链表头
block := finc
finc = block.next
// 将从 finc 获取到的链表挂载到 finq 的队列头,finq 指向新的 block
block.next = finq
finq = block
}
// 根据 finq.cnt 获取索引对应的 block
f := &finq.fin[finq.cnt]
atomic.Xadd(&finq.cnt, +1) // Sync with markroots
// 设置相关属性
f.fn = fn
f.nret = nret
f.fint = fint
f.ot = ot
f.arg = p
// 设置唤醒标志
fingwake = true
unlock(&finlock)
}
至此,也就明白了,runq 全局队列是怎么被填充的了
那么,第二个问题,当 fing 被休眠后,怎么被唤醒呢?
这里就需要追溯到,深入理解 Go-goroutine 的实现及 Scheduler 分析 这篇文章了
2.2.4. findrunnable
在 findrunnable 中有一段代码如下:
func findrunnable() (gp *g, inheritTime bool) {
// 通过状态位判断是否需要唤醒 fing,通过 wakefing 来判断并返回 fing
if fingwait && fingwake {if gp := wakefing(); gp != nil {
// 唤醒 g,并从休眠出继续执行
ready(gp, 0, true)
}
}
}
2.2.5. wakefing
这里不仅会对状态位 fingwait fingwake 做二次判断,而且,如果状态位符合唤醒要求的话,需要重置两个状态位
func wakefing() *g {
var res *g
lock(&finlock)
if fingwait && fingwake {
fingwait = false
fingwake = false
res = fing
}
unlock(&finlock)
return res
}
3. 参考文档
- 《Go 语言学习笔记》– 雨痕