Go的runtime.SetFinalizer函数介绍

3,576 阅读4分钟

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

介绍

  1. runtime.SetFinalizer 是Go提供对象被GC回收时的一个注册函数,可以在对象被回收的时候回掉函数
  2. 此方法类似于JAVAfinalize 方法和C++析构函数
  3. 当存在多层引用时,类似于A->B->C 这种关系的时候,是如何解决呢?
  4. Go函数内部原理介绍
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

简单使用

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函数,但是没有被回收!我就犯过这样的错误,不过在写测试用例的时候就发现了!!
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

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)
	}
}