

深入理解Go-runtime.SetFinalizer原理剖析
source link: https://tyloafer.github.io/posts/23555/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

finalizer是与对象关联的一个函数,通过runtime.SetFinalizer
来设置,它在对象被GC的时候,这个finalizer会被调用,以完成对象生命中最后一程。由于finalizer的存在,导致了对象在三色标记中,不可能被标为白色对象,也就是垃圾,所以,这个对象的生命也会得以延续一个GC周期。正如defer一样,我们也可以通过 Finalizer 完成一些类似于资源释放的操作
type mspan struct {
// 当前span上所有对象的special串成链表
// special中有个offset,就是数据对象在span上的offset,通过offset,将数据对象和special关联起来
specials *special // linked list of special records sorted by offset.
}
special
type special struct {
next *special // linked list in span
// 数据对象在span上的offset
offset uint16 // span offset of object
kind byte // kind of special
}
##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.
}
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)
}
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
创建finalizer
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)
}
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")
}
})
}
这里逻辑没什么复杂的,只是在参数、类型的判断等上面,比较的麻烦
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也是一样的
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
}
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
}
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
}
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()
}
}
执行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
中有下面一段函数
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)
}
....
}
....
}
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")
}
}
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分析 这篇文章了
findrunnable
在 findrunnable 中有一段代码如下:
func findrunnable() (gp *g, inheritTime bool) {
// 通过状态位判断是否需要唤醒 fing, 通过wakefing来判断并返回fing
if fingwait && fingwake {
if gp := wakefing(); gp != nil {
// 唤醒g,并从休眠出继续执行
ready(gp, 0, true)
}
}
}
wakefing
这里不仅会对状态位 fingwait fingwake做二次判断,而且,如果状态位符合唤醒要求的话,需要重置两个状态位
func wakefing() *g {
var res *g
lock(&finlock)
if fingwait && fingwake {
fingwait = false
fingwake = false
res = fing
}
unlock(&finlock)
return res
}
- 《Go语言学习笔记》–雨痕
Recommend
-
12
Redis作为目前通用的缓存选型,因其高性能而倍受欢迎。Redis的2.x版本仅支持单机模式,从3.0版本开始引入集群模式。 Redis的Java生态的客户端当中包含Jedis、Redisson、Lettuce,不同的客户端具备不同的能力是使用方式,本文主要分析Jedis客户端。...
-
12
联系方式(qq):623847465 gitee(码云): Mercury. (zzwlwp) - Gitee.com 代码千万行,注释第一行;命名不规范,同事泪两行。🤪 爱打篮球的程序员温馨提示:规范代...
-
6
深入理解Go-map原理剖析 发表于 2019-10-08 ...
-
5
作者 永超 (Robin) 发表于 2020-07-01 14 分钟阅读在Objective-C中,可以通过Category添加属性、方法、协议,在R...
-
10
Defer 也是Go里面比较特别的一个关键字了,主要就是用来保证在程序执行过程中,defer后面的函数都会被执行到,一般用来关闭连接、清理资源等。 defertype _defer struct { siz int32 // 参数的大小 star...
-
9
Synchronized锁升级原理与过程深入剖析 在上篇文章
-
9
完全二叉树:完全二叉树是满二叉树去除最后N个节点之后得到的树(N≥0,N∈N∗)
-
4
我们常见的并发锁ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。 上...
-
4
我们常见的并发锁ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。上篇文章讲了AQS的加锁流程,这篇文章再一块看一下AQS具体源码实现。先回顾一下AQS的加锁流程
-
7
Openmp Runtime 库函数汇总(下)——深入剖析锁🔒原理与实现 在本篇文章当中主要给大家介绍一下 OpenMP 当中经常使用到的锁并且仔细分析它其中的内部原理!在 OpenMP 当中主要有两种类型的锁,一个是...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK