当我们希望在对象被gc之前,做一些资源释放的工作,类似,函数返回之前执行defer操作一样,我们可以使用 runtime.SetFinalizer,它会在gc周期到来的时候,检查下对象有没有引用,如果没有引用,起一个协程,执行绑定的资源释放函数。执行完毕后解除绑定,当下一个gc周期到来的时候回收当前对象。
代码语言:javascript复制func SetFinalizer(obj any, finalizer any) {
的定义位于src/runtime/mfinal.go。详细信息可以看下它的注释,很完备。
通过分析具体注释我们可以总结出下面三点:
* 即使程序正常结束或者发生错误, 但是在对象被 gc 选中并被回收之前,SetFinalizer 都不会执行, 所以不要在SetFinalizer中执行将内存中的内容flush到磁盘这种操作。
* SetFinalizer 最大的问题是延长了对象生命周期。在第一次回收时执行 Finalizer 函数,且目标对象重新变成可达状态,直到第二次才真正 “销毁”。这对于有大量对象分配的高并发算法,可能会造成很大麻烦
* 指针构成的 "循环引⽤" 加上 runtime.SetFinalizer 会导致内存泄露
下面我们结合源码看下:
代码语言:javascript复制package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// i 就是后面说的 数据对象
var i = 3
// 这里的func 就是后面一直说的 finalizer
runtime.SetFinalizer(&i, func(i *int) {
fmt.Println(i, *i, "set finalizer")
})
runtime.GC()
time.Sleep(time.Second * 1)
runtime.GC() //0xc000018168 3 set finalizer
subFunc()
time.Sleep(time.Second * 1)
runtime.GC() //0xc000018180 3 set sub finalizer
}
func subFunc() {
// i 就是后面说的 数据对象
var i = 3
// 这里的func 就是后面一直说的 finalizer
runtime.SetFinalizer(&i, func(i *int) {
fmt.Println(i, *i, "set sub finalizer")
})
runtime.GC()
}
我们强制gc后,由于没有引用,所以会执行finalize函数,但是下面的场景,由于存在相互引用,则会出现内存泄漏,不论我们强制多少次gc,都不会起作用。
代码语言:javascript复制package main
import (
"fmt"
"runtime"
)
func main() {
memoryLeaking()
runtime.GC()
runtime.GC()
runtime.GC()
}
func memoryLeaking() {
type T struct {
v [1 << 20]int
t *T
}
var finalizer = func(t *T) {
fmt.Println("finalizer called")
}
var x, y T
// The SetFinalizer call makes x escape to heap.
runtime.SetFinalizer(&x, finalizer)
// The following line forms a cyclic reference
// group with two members, x and y.
// This causes x and y are not collectable.
x.t, y.t = &y, &x // y also escapes to heap.
}