0%

Go的runtime.SetFinalizer函数介绍

​ 业务中我们经常遇到需要进行手动回收的操作,虽然Go提供了defer操作可以用来手动回收,但是有些时候确实会出现一些case用户忘记手动回收,并且大量内存泄漏或者goroutine泄口的问题,而且只能通过线上工具进行事后定位!本文介绍一下 runtime.SetFinalizer 来解决对象回收释放资源的问题!本文只是根据简单的例子进行阐述,例子选择不一定的好!

介绍

  1. runtime.SetFinalizer 是Go提供对象被GC回收时的一个注册函数,可以在对象被回收的时候回掉函数
  2. 此方法类似于JAVAfinalize 方法和C++析构函数
  3. 当存在多层引用时,类似于A->B->C 这种关系的时候,是如何解决呢?
  4. Go函数内部原理介绍
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
func SetFinalizer(obj interface{}, finalizer interface{}) {
....
// 对象的低5位是对象类型,这里检测一下是否是指针类型
if etyp.kind&kindMask != kindPtr {
throw("runtime.SetFinalizer: first argument is " + etyp.string() + ", not pointer")
}
// 第二个参数必须是函数
if ftyp.kind&kindMask != kindFunc {
throw("runtime.SetFinalizer: second argument is " + ftyp.string() + ", not a function")
}
//
// make sure we have a finalizer goroutine
createfing()

// finally add finalizer
systemstack(func() {
if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {
throw("runtime.SetFinalizer: finalizer already set")
}
})
}

// 最后启动调度池子,也就是只有一个G去回收整个应用程序的finalize函数!
func createfing() {
// start the finalizer goroutine exactly once
if fingCreate == 0 && atomic.Cas(&fingCreate, 0, 1) {
go runfinq()
}
}

// 1. addfinalizer 就是给对象的指针指向的内存加了个特殊标记!此标记此对象是finalizer对象,内部实现就是拿到对象的span,然后span里面有个链表维护对象的特殊标记!
// 2. runfinq 函数,就是遍历一个队列,然后回收队列中的对象,一个死循环罢了!如果没有等待回收的对象,就park住
// 3. 每次当GC sweep 阶段,会先标记,然后第二次GC才要被回收(清理)!具体逻辑可以看mspan#sweep

简单使用

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"fmt"
"runtime"
"time"
)

type object int

func (o object) Ptr() *object {
return &o
}

var (
cacheData = make(map[string]*object, 1024)
)

func deleteData(key string) {
delete(cacheData, key)
}

func setData(key string, v object) {
data := v.Ptr()
runtime.SetFinalizer(data, func(data *object) {
fmt.Printf("runtime invoke Finalizer data: %d, time: %s\n", *data, time.Now().Format("15:04:05.000"))
time.Sleep(time.Second)
})
cacheData[key] = data
}

func main() {
setData("key1", 1)
setData("key2", 2)
setData("key3", 3)

deleteData("key1")
deleteData("key2")
deleteData("key3")

for x := 0; x < 5; x++ {
fmt.Println("invoke runtime.GC()")
runtime.GC()
time.Sleep(time.Second)
}
}

// output:
//invoke runtime.GC()
//runtime invoke Finalizer data: 1, time: 23:44:49.013
//invoke runtime.GC()
//runtime invoke Finalizer data: 3, time: 23:44:50.019
//invoke runtime.GC()
//runtime invoke Finalizer data: 2, time: 23:44:51.020
//invoke runtime.GC()
//invoke runtime.GC()
  1. 并没有看出第二次才会GC掉,可能是系统在delele过程中触发过一次GC
  2. 可以看到GC后调用Finalizer 函数是串行执行的!

日常使用注意点

  1. GC注册的Finalizer函数执行时间不适合过长!
  2. Finalizer 函数返回结果是系统会忽略,所以你返回error也无所谓,但是切记不可以panic,程序是无法recover的!
  3. 如果对象在Finalizer函数再次被引用,是不会被再次回收调用Finalizer函数的!
  4. 当存在 A->B->C 的引用时,回收顺序是引用顺序,当回收A后,然后再回收B,然后再回收C,应该没啥问题吧
  5. 如果你对象被goroutine 引用而分配到堆上,goroutine 又没办法关闭,导致你需要包装一层对象进行回收!例如下列例子,导致业务函数结束后忘记了Close,导致G泄漏,虽然注册了Finalizer函数,但是没有被回收!我就犯过这样的错误,不过在写测试用例的时候就发现了!!
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
type cacheData struct {
name string
dataLock sync.RWMutex
data map[string]interface{}
reporter func(data *cacheData)

closeOnce sync.Once
done chan struct{}
}

func NewCacheData(name string) *cacheData {
data := &cacheData{
name: name,
data: map[string]interface{}{},
reporter: func(data *cacheData) {
log.Println("reporter")
},
done: make(chan struct{}, 0),
}
data.init()
runtime.SetFinalizer(data, (*cacheData).Close)
return data
}

// init 注册reporter函数,比如上报一些缓存的信息
func (c *cacheData) init() {
go func() {
c.reporter(c)
t := time.NewTicker(time.Second)
for {
select {
case <-c.done:
t.Stop()
return
case <-t.C:
c.reporter(c)
}
}
}()
}

// Close 函数主要是防止goroutine泄漏
func (c *cacheData) Close() {
c.closeOnce.Do(func() {
close(c.done)
})
}

func BizFunc() {
cache := NewCacheData("test")

cache.Set("k1", "v1")

// biz ....
// 但是忘记关闭cache了,或者等等的没有close,导致G泄漏
}

func main() {
BizFunc()
for x := 0; x < 10; x++ {
runtime.GC()
log.Println("runtime.GC")
time.Sleep(time.Second)
}
}

如何解决了?? 可以看 NewSafeCacheData

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package main

import (
"log"
"runtime"
"sync"
"time"
)

func init() {
log.SetFlags(log.Ltime)
}
func (c *cacheData) Set(key string, v interface{}) {
c.dataLock.Lock()
defer c.dataLock.Unlock()
c.data[key] = v
}

type CacheData struct {
*cacheData
}

type cacheData struct {
name string
dataLock sync.RWMutex
data map[string]interface{}
reporter func(data *cacheData)

closeOnce sync.Once
done chan struct{}
}

func NewCacheData(name string) *cacheData {
data := &cacheData{
name: name,
data: map[string]interface{}{},
reporter: func(data *cacheData) {
log.Println("reporter")
},
done: make(chan struct{}, 0),
}
return data
}

// NewSafeCacheData 安全的函数
func NewSafeCacheData(name string) *CacheData {
data := NewCacheData(name)
data.init()
result := &CacheData{
cacheData: data,
}
runtime.SetFinalizer(result, (*CacheData).Close)
return result
}

// init 注册reporter函数,比如上报一些缓存的信息
func (c *cacheData) init() {
go func() {
c.reporter(c)
t := time.NewTicker(time.Second)
for {
select {
case <-c.done:
t.Stop()
return
case <-t.C:
c.reporter(c)
}
}
}()
}

// Close 函数主要是防止goroutine泄漏
func (c *cacheData) Close() {
c.closeOnce.Do(func() {
close(c.done)
})
}

func BizFunc() {
cache := NewSafeCacheData("test")

cache.Set("k1", "v1")

// biz ....
// 但是忘记关闭cache了,或者等等的没有close,导致G泄漏
}

func main() {
BizFunc()
for x := 0; x < 10; x++ {
runtime.GC()
log.Println("runtime.GC")
time.Sleep(time.Second)
}
}
本人坚持原创技术分享,如果你觉得文章对您有用,请随意打赏! 如果有需要咨询的请发送到我的邮箱!