解析
如果我们希望在一个对象被gc之前,做一些资源释放的工作,我们可以使用 runtime.SetFinalizer
。就像函数返回之前执行defer释放资源一样。比如下面的代码:
This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.
List1: example By runtime.SetFinalizer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
type MyStruct struct {
Name string
Other *MyStruct
}
func main() {
x := MyStruct{Name: "X"}
runtime.SetFinalizer(&x, func(x *MyStruct) {
fmt.Printf("Finalizer for %s is called\n", x.Name)
})
runtime.GC()
time.Sleep(1 * time.Second)
runtime.GC()
}
|
官方文档中对SetFinalizer的一些解释,主要含义是对象可以关联一个SetFinalizer函数, 当GC
检测到unreachable对象有关联的SetFinalizer函数时,会执行关联的SetFinalizer函数, 同时取消关联。 这样当下一次GC
的时候,对象重新处于unreachable
状态并且没有SetFinalizer关联, 就会被回收。
仔细看文档,还有几个需要注意的点:
- 即使程序正常结束或者发生错误, 但是在对象被 gc 选中并被回收之前,SetFinalizer 都不会执行, 所以不要在
SetFinalizer
中执行将内存中的内容flush到磁盘这种操作。
SetFinalizer
最大的问题是延长了对象生命周期。在第一次回收时执行 Finalizer 函数,且目标对象重新变成可达状态,直到第二次才真正 “销毁”。这对于有大量对象分配的高并发算法,可能会造成很大麻烦
- 指针构成的 “循环引⽤” 加上
runtime.SetFinalizer
会导致内存泄露。
list 2: runtime.SetFinalizer
memory leak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// MyStruct 是一个简单的结构体,包含一个指针字段。
type MyStruct struct {
Name string
Other *MyStruct
}
func main() {
x := MyStruct{Name: "X"}
y := MyStruct{Name: "Y"}
x.Other = &y
y.Other = &x
runtime.SetFinalizer(&x, func(x *MyStruct) {
fmt.Printf("Finalizer for %s is called\n", x.Name)
})
time.Sleep(time.Second)
runtime.GC()
time.Sleep(time.Second)
runtime.GC()
}
|
x 永远不会被释放。正确的做法应该是, 在不需要使用 对象的时候,显式移除 Finalizer runtime.SetFinalizer(&x, nil)
实际应用
在业务代码中很少使用runtime.SetFinalizer
(我没使用过)但是再Go源码中 有比较多的使用, 比如
net/http
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
runtime.SetFinalizer(fd, (*netFD).Close)
}
func (fd *netFD) Close() error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.Close()
}
runtime.SetFinalizer(fd, nil)
return fd.pfd.Close()
}
|
go-cache库提供了SetFinalizer的一种用法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
items := make(map[string]Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
c := newCache(de, m)
C := &Cache{c}
if ci > 0 {
runJanitor(c, ci)
runtime.SetFinalizer(C, stopJanitor)
}
return C
}
func runJanitor(c *cache, ci time.Duration) {
j := &janitor{
Interval: ci,
stop: make(chan bool),
}
c.janitor = j
go j.Run(c)
}
func stopJanitor(c *Cache) {
c.janitor.stop <- true
}
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.Interval)
for {
select {
case <-ticker.C:
c.DeleteExpired()
case <-j.stop:
ticker.Stop()
return
}
}
}
|
newCacheWithJanitor
在ci参数大于0时,将开启后台协程,通过ticker定期清理过期缓存。一旦从stop chan中读到值,则异步协程退出。
stopJanitor
为指向Cache的指针C定义了finalizer函数stopJanitor
。一旦我们在业务代码中不再有指向Cache的引用时,c
将会进行GC
流程,首先执行stopJanitor
函数,其作用是为内部的stop channel
写入值,从而通知上一步的异步清理协程,使其退出。这样就实现了业务代码无感知的异步协程回收,是一种优雅的退出方式。