golang源码分析:runtime.SetFinalizer

2023-09-06 19:26:19 浏览数 (2)

当我们希望在对象被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.
}

0 人点赞