什么是Finalizer
In computer science, a finalizer or finalize method is a special method that performs finalization, generally some form of cleanup.
Finalizer通常用来执行一些清理操作。
finalizer(终结器) 和 destructor(析构函数)
The terminology of "finalizer" and "finalization" versus "destructor" and "destruction" varies between authors and is sometimes unclear.
In common usage, a destructor is a method called deterministically on object destruction, and the archetype is C++ destructors; while a finalizer is called non-deterministically by the garbage collector, and the archetype is Java
finalize
methods.
析构函数是在对象销毁时确定性调用的方法,原型是 C++ 析构函数;而垃圾收集器不确定地调用终结器,并且原型是 Javafinalize
方法。
析构函数的调用时间点是确定的,拿C++举例,当跳出class声明作用域或者显式调用delete执行清理时,如果该class存在析构函数,即会在当前线程执行。
终结器的调用时间不是确定的,由垃圾回收期决定何时执行,可以保证的是在该对象真正被回收之前会执行其注册的finalize
,在哪个线程上执行也是不确定的。
Go中的Finalizer
如何使用
推荐用法
package main
import (
"log"
"runtime"
"time"
)
type test int
func findRoad(t *test) {
// 一般在这里进行资源回收
log.Println("test:", *t)
}
func entry() {
var rd test = test(1111)
r := &rd
// 解除绑定并执行对应函数下一次gc在进行清理
runtime.SetFinalizer(r, findRoad)
}
func main() {
entry()
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
runtime.GC()
}
}
// OUTPUT:
// 2021/08/26 23:22:29 test: 1111
runtime
中提供了runtime.SetFinalizer
来为对象注册Finalizer
函数
该函数有两个入参
- 入参一为注册Finalizer的对象地址
- 入参二为 被"终结"时要执行的回调函数,该函数正常情况下应接受一个与注册对象类型相同的指针,如上方例子中的findRoad
当然也可以用它来做一些骚操作
不太推荐的用法
如上方所述,Finalizer是在已经确定该对象不再存活的情况下,被回收之前执行的,那么我们可以在Finalizer中让这个本该被回收的对象,再“活过来”
package main
import (
"log"
"runtime"
"time"
)
type test int
func findRoad(t *test) {
log.Println("test:", *t)
*t = 3
global = t
}
var global interface{}
func entry() {
var rd test = test(1)
r := &rd
go func() {
rd = 2
}()
runtime.SetFinalizer(r, findRoad)
}
func main() {
entry()
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
runtime.GC()
}
log.Println("main:", *(global).(*test))
}
// OUTPUT:
// 2021/08/26 23:28:57 test: 2
// 2021/08/26 23:29:00 main: 3
禁止使用的用法
当然也可以在Finalizer函数中调用会导致协程阻塞的方法,比如说Sleep()
、Mutex.lock()
注:这个例子仅用来说明在Go中 Finalizer是在协程环境中执行的,实际使用中Finalizer中仅应该做一些快速简单的清理操作,否则会阻塞其他Finalizer的执行(源码中可以证明这一点)。
package main
import (
"log"
"runtime"
"sync"
"time"
)
type test int
func findRoad(t *test) {
log.Println("test:", *t)
time.Sleep(time.Second)
log.Println("Sleep Done")
}
func entry() {
var rd test = test(1)
r := &rd
runtime.SetFinalizer(r, findRoad)
}
func main() {
entry()
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
runtime.GC()
}
}
// OUTPUT:
// 2021/08/26 23:42:35 test: 1
// 2021/08/26 23:42:36 Sleep Done
标准库中的使用?
实现原理
TODO:总结
设置
TODO:mspan简单介绍
// $GOROOT/src/runtime/mfinal.go
func SetFinalizer(obj interface{}, finalizer interface{}) {
e := efaceOf(&obj)
// 去除参数校验逻辑
f := efaceOf(&finalizer)
ftyp := f._type
okarg:
// compute size needed for return parameters
nret := uintptr(0)
for _, t := range ft.out() {
nret = alignUp(nret, uintptr(t.align)) + uintptr(t.size)
}
nret = alignUp(nret, sys.PtrSize)
// 确保已创建finalizer goroutine
createfing()
// 切换到系统栈执行addfinalizer
systemstack(func() {
if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {
throw("runtime.SetFinalizer: finalizer already set")
}
})
}
func createfing() {
// 使用CAS来拿到执行权,保证只创建一个goroutine
if fingCreate == 0 && atomic.Cas(&fingCreate, 0, 1) {
go runfinq()
}
}
// $GOROOT/src/runtime/mheap.go
// Adds a finalizer to the object p. Returns true if it succeeded.
func addfinalizer(p unsafe.Pointer, f *funcval, nret uintptr, fint *_type, ot *ptrtype) bool {
lock(&mheap_.speciallock)
// 申请一个 specialfinalizer
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) {
// GC期间调用SetFinalizer,会继续标记它的Filed
if gcphase != _GCoff {
base, _, _ := findObject(uintptr(p), 0, 0)
mp := acquirem()
gcw := &mp.p.ptr().gcw
// Mark everything reachable from the object
// so it's retained for the finalizer.
scanobject(base, gcw)
// Mark the finalizer itself, since the
// special isn't part of the GC'd heap.
scanblock(uintptr(unsafe.Pointer(&s.fn)), sys.PtrSize, &oneptrmask[0], gcw, nil)
releasem(mp)
}
return true
}
// There was an old finalizer
lock(&mheap_.speciallock)
mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
unlock(&mheap_.speciallock)
return false
}
func addspecial(p unsafe.Pointer, s *special) bool {
// 根据p找到对应的mspan
span := spanOfHeap(uintptr(p))
mp := acquirem()
span.ensureSwept()
// 计算在对应mspan中的偏移,可以根据这个信息反查到对应的对象
offset := uintptr(p) - span.base()
kind := s.kind
lock(&span.speciallock)
// Find splice point, check for existing record.
// 添加到span.specials链表中
t := &span.specials
for {
x := *t
if x == nil {
break
}
if offset == uintptr(x.offset) && kind == x.kind {
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.
s.offset = uint16(offset)
s.next = *t
*t = s
spanHasSpecials(span)
unlock(&span.speciallock)
releasem(mp)
return true
}
触发
在GC mark和sweep期间都需要进行对应的处理
mark:每个mspan的specials链表是GC中的根节点,对设置了Finalizer的对象的子field进行扫描标记,但自身不会在扫描根节点过程中被标记,自身是否存活是根据从其他root节点是否可达来确定的。
标记其子field是因为,如果本轮GC过程中设置了Finalizer的对象不可达,即会执行所设置的Finalizer回调函数,并且该对象自身会被作为入参传入。而执行回调的时机是在sweep清扫阶段,所以需要对其子field进行保活,这也就导致了后续的“循环引用和Finalizer一起使用会导致内存泄漏”的问题
// $GOROOT/src/runtime/mgcmark.go
func markroot(gcw *gcWork, i uint32) {
// Note: if you add a case here, please also update heapdump.go:dumproots.
switch {
case work.baseSpans <= i && i < work.baseStacks:
// mark mspan.specials
markrootSpans(gcw, int(i-work.baseSpans))
default:
}
}
func markrootSpans(gcw *gcWork, shard int) {
// 找到对应mspan
// Construct slice of bitmap which we'll iterate over.
specialsbits := ha.pageSpecials[arenaPage/8:]
specialsbits = specialsbits[:pagesPerSpanRoot/8]
for i := range specialsbits {
// Find set bits, which correspond to spans with specials.
specials := atomic.Load8(&specialsbits[i])
if specials == 0 {
continue
}
for j := uint(0); j < 8; j++ {
// Lock the specials to prevent a special from being
// removed from the list while we're traversing it.
lock(&s.speciallock)
// 遍历mspan中的specials
for sp := s.specials; sp != nil; sp = sp.next {
// 只关心_KindSpecialFinalizer
if sp.kind != _KindSpecialFinalizer {
continue
}
// don't mark finalized object, but scan it so we
// retain everything it points to.
spf := (*specialfinalizer)(unsafe.Pointer(sp))
// A finalizer can be set for an inner byte of an object, find object beginning.
p := s.base() + uintptr(spf.special.offset)/s.elemsize*s.elemsize
// Mark everything that can be reached from
// the object (but *not* the object itself or
// we'll never collect it).
scanobject(p, gcw)
// The special itself is a root.
scanblock(uintptr(unsafe.Pointer(&spf.fn)), sys.PtrSize, &oneptrmask[0], gcw, nil)
}
unlock(&s.speciallock)
}
}
}
func (sl *sweepLocked) sweep(preserve bool) bool {
// It's critical that we enter this function with preemption disabled,
// GC must not start while we are in the middle of this function.
_g_ := getg()
s := sl.mspan
hadSpecials := s.specials != nil
siter := newSpecialsIter(s)
// 遍历specials链表
for siter.valid() {
// A finalizer can be set for an inner byte of an object, find object beginning.
// 根据offset反查对应的object在mspan中的位置
objIndex := uintptr(siter.s.offset) / size
p := s.base() + objIndex*size
mbits := s.markBitsForIndex(objIndex)
// 如果该object没有被标记,说明在这轮GC中是不可以对象,可以被回收
// 触发Finalizer机制
if !mbits.isMarked() {
// This object is not marked and has at least one special record.
// Pass 1: see if it has at least one finalizer.
hasFin := false
endOffset := p - s.base() + size
for tmp := siter.s; tmp != nil && uintptr(tmp.offset) < endOffset; tmp = tmp.next {
if tmp.kind == _KindSpecialFinalizer {
// Stop freeing of object if it has a finalizer.
// 标记该对象,使其多存活一周期
mbits.setMarkedNonAtomic()
hasFin = true
break
}
}
// Pass 2: queue all finalizers _or_ handle profile record.
// 从special列表移除该记录,下轮GC时,该object已经不再拥有Finalizer
for siter.valid() && uintptr(siter.s.offset) < endOffset {
// Find the exact byte for which the special was setup
// (as opposed to object beginning).
special := siter.s
p := s.base() + uintptr(special.offset)
if special.kind == _KindSpecialFinalizer || !hasFin {
siter.unlinkAndNext()
freeSpecial(special, unsafe.Pointer(p), size)
} else {
// The object has finalizers, so we're keeping it alive.
// All other specials only apply when an object is freed,
// so just keep the special record.
siter.next()
}
}
} else {
// object is still live
if siter.s.kind == _KindSpecialReachable {
// 测试用
special := siter.unlinkAndNext()
(*specialReachable)(unsafe.Pointer(special)).reachable = true
freeSpecial(special, unsafe.Pointer(p), size)
} else {
// 对象存活,保留该Finalizer,继续遍历
// keep special record
siter.next()
}
}
}
if hadSpecials && s.specials == nil {
spanHasNoSpecials(s)
}
}
// freeSpecial performs any cleanup on special s and deallocates it.
// s must already be unlinked from the specials list.
func freeSpecial(s *special, p unsafe.Pointer, size uintptr) {
switch s.kind {
case _KindSpecialFinalizer:
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)
case _KindSpecialReachable:
sp := (*specialReachable)(unsafe.Pointer(s))
sp.done = true
// The creator frees these.
default:
throw("bad special kind")
panic("not reached")
}
}
// 提交到finq队列,并设置fingwake
func queuefinalizer(p unsafe.Pointer, fn *funcval, nret uintptr, fint *_type, ot *ptrtype) {
if gcphase != _GCoff {
// Currently we assume that the finalizer queue won't
// grow during marking so we don't have to rescan it
// during mark termination. If we ever need to lift
// this assumption, we can do it by adding the
// necessary barriers to queuefinalizer (which it may
// have automatically).
throw("queuefinalizer during GC")
}
lock(&finlock)
if finq == nil || finq.cnt == uint32(len(finq.fin)) {
if finc == nil {
finc = (*finblock)(persistentalloc(_FinBlockSize, 0, &memstats.gcMiscSys))
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)]
}
}
}
block := finc
finc = block.next
block.next = finq
finq = 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)
}
执行
// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from local or global queue, poll network.
func findrunnable() (gp *g, inheritTime bool) {
_g_ := getg()
// The conditions here and in handoffp must agree: if
// findrunnable would return a G to run, handoffp must start
// an M.
top:
_p_ := _g_.m.p.ptr()
// 调度过程中发现需要执行Finalizer,唤醒之前创建的runfinq()
if fingwait && fingwake {
if gp := wakefing(); gp != nil {
ready(gp, 0, true)
}
}
}
// This is the goroutine that runs all of the finalizers
func runfinq() {
var (
frame unsafe.Pointer
framecap uintptr
argRegs int
)
for {
lock(&finlock)
fb := finq
finq = nil
if fb == nil {
gp := getg()
fing = gp
fingwait = true
goparkunlock(&finlock, waitReasonFinalizerWait, traceEvGoBlock, 1)
continue
}
argRegs = intArgRegs
unlock(&finlock)
if raceenabled {
racefingo()
}
for fb != nil {
for i := fb.cnt; i > 0; i-- {
// 从队列中取出待执行的任务
f := &fb.fin[i-1]
var regs abi.RegArgs
var framesz uintptr
if argRegs > 0 {
// The args can always be passed in registers if they're
// available, because platforms we support always have no
// argument registers available, or more than 2.
//
// But unfortunately because we can have an arbitrary
// amount of returns and it would be complex to try and
// figure out how many of those can get passed in registers,
// just conservatively assume none of them do.
framesz = f.nret
} else {
// Need to pass arguments on the stack too.
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")
}
r := frame
if argRegs > 0 {
r = unsafe.Pointer(®s.Ints)
} else {
// frame is effectively uninitialized
// memory. That means we have to clear
// it before writing to it to avoid
// confusing the write barrier.
*(*[2]uintptr)(frame) = [2]uintptr{}
}
switch f.fint.kind & kindMask {
case kindPtr:
// direct use of pointer
*(*unsafe.Pointer)(r) = f.arg
case kindInterface:
ityp := (*interfacetype)(unsafe.Pointer(f.fint))
// set up with empty interface
(*eface)(r)._type = &f.ot.typ
(*eface)(r).data = f.arg
if len(ityp.mhdr) != 0 {
// convert to interface with methods
// this conversion is guaranteed to succeed - we checked in SetFinalizer
(*iface)(r).tab = assertE2I(ityp, (*eface)(r)._type)
}
default:
throw("bad kind in runfinq")
}
fingRunning = true
// 使用反射的方式调用注册的回调函数
// 可以看到后台只有这一个goroutine在勤勤恳恳按顺序的执行Finalizer回调
// 所以在Finalizer中调用阻塞函数是一件非常危险的事情
reflectcall(nil, unsafe.Pointer(f.fn), frame, uint32(framesz), uint32(framesz), uint32(framesz), ®s)
fingRunning = false
// Drop finalizer queue heap references
// before hiding them from markroot.
// This also ensures these will be
// clear if we reuse the finalizer.
f.fn = nil
f.arg = nil
f.ot = nil
atomic.Store(&fb.cnt, i-1)
}
next := fb.next
lock(&finlock)
fb.next = finc
finc = fb
unlock(&finlock)
fb = next
}
}
}
为什么和循环引用一起使用会导致内存泄漏?
这个应该也属于禁止使用的用法
package main
import (
"log"
"runtime"
"time"
)
type X struct {
data [1 << 20][10]byte // 构造大对象,使其逃逸到堆上
ptr *X
}
func test() {
var a, b X
a.ptr = &b
b.ptr = &a
runtime.SetFinalizer(&a, func(*X) { log.Println("Finalizer a") })
runtime.SetFinalizer(&b, func(*X) { log.Println("Finalizer b") })
}
func main() {
for i := 0; i < 10; i++ {
test()
runtime.GC()
time.Sleep(time.Second)
}
}
// OUTPUT:
// gc 1 @0.001s 6%: 0.009+1.3+0.002 ms clock, 0.072+0.10/1.3/1.2+0.018 ms cpu, 10->10->10 MB, 11 MB goal, 8 P
// gc 2 @0.003s 15%: 0.007+3.8+0.002 ms clock, 0.062+0/7.4/18+0.016 ms cpu, 20->20->60 MB, 21 MB goal, 8 P
// gc 3 @0.007s 17%: 0.015+2.7+0.002 ms clock, 0.12+0/5.4/5.3+0.021 ms cpu, 60->60->40 MB, 120 MB goal, 8 P (forced)
// gc 4 @1.021s 0%: 0.068+5.6+0.002 ms clock, 0.54+0/11/16+0.019 ms cpu, 60->60->80 MB, 80 MB goal, 8 P (forced)
// gc 5 @2.032s 0%: 0.054+10+0.002 ms clock, 0.43+0/21/61+0.021 ms cpu, 100->100->120 MB, 160 MB goal, 8 P (forced)
可以看出,Finalizer并没有被执行,并且GC无法回收掉函数中的局部变量。
注:解除a和b的循环引用关系,或者不对其SetFinalizer都可以正常回收,这并不是GC的问题。
Finalizers are run in dependency order: if A points at B, both have finalizers, and they are otherwise unreachable, only the finalizer for A runs; once A is freed, the finalizer for B can run. If a cyclic structure includes a block with a finalizer, that cycle is not guaranteed to be garbage collected and the finalizer is not guaranteed to run, because there is no ordering that respects the dependencies.
其实主要是因为Finalizers也会作为GC的根节点,会进行标记它的子Field,所以在这种场景下,a和b在每轮GC中都是存活的(a是通过b的Finalizers标记存活,b是通过a的Finalizers标记存活),也就导致了内存泄漏,在sweep期间,对于存活对象也不会调用其Finalizers函数,所以Finalizers回调也并不会被执行。
参考链接
深入理解Go-runtime.SetFinalizer原理剖析
本文由 LeonardWang 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Aug 27,2021