Go runtime 提供了一種允許開發(fā)者將一個函數(shù)與一個變量綁定的方法 runtime.SetFinalizer,被綁定的變量從它無法被訪問時就被垃圾回收器視為待回收狀態(tài)。這個特性引起了高度的爭論,但本文并不打算參與其中,而是去闡述這個方法的具體實(shí)現(xiàn)。
無保障性
舉一個使用了 Finalizer 的例子
package main import ( "fmt" "math/rand" "runtime" "strconv" "time" ) type Foo struct { a int } func main() { for i := 0; i < 3; i++ { f := NewFoo(i) println(f.a) } runtime.GC() } //go:noinline func NewFoo(i int) *Foo { f := &Foo{a: rand.Intn(50)} runtime.SetFinalizer(f, func(f *Foo) { fmt.Println(`foo ` + strconv.Itoa(i) + ` has been garbage collected`) }) return f }
這段程序?qū)谶@個循環(huán)中創(chuàng)建三個 struct 的的實(shí)例,并將每個實(shí)例都綁定一個 finalizer。之后垃圾回收器將會被調(diào)用,并回收之前創(chuàng)建的實(shí)例。運(yùn)行這個程序,將會給到我們?nèi)缦螺敵觯?
31 37 47
如我們所見,finalizers 并沒有被調(diào)用,runtime 的文檔解釋這一點(diǎn):
在程序無法獲取到一個 obj 所指向的對象后的任意時刻,finalizer 被調(diào)度運(yùn)行,且無法保證 finalizer 運(yùn)行在程序退出之前。因此一般情況下,因此它們僅用于在長時間運(yùn)行的程序上釋放一些與對象關(guān)聯(lián)的非內(nèi)存資源。
在調(diào)用 finalizer 之前,runtime 不提供有關(guān)延遲的任何保證。讓我們試著去修改我們的程序,通過在調(diào)用垃圾回收器之后添加一個一秒的 sleep:
31 37 47 foo 1 has been garbage collected foo 0 has been garbage collected
現(xiàn)在我們的 finalizer 已經(jīng)被調(diào)用了,然而,它們其中一個消失了。我們的 finalizers 與垃圾回收器相連接,并且垃圾回收器回收以及清理數(shù)據(jù)的方式將會對 finalizers 的調(diào)用產(chǎn)生影響。
工作流
之前的例子可能讓我認(rèn)為 Go 僅在釋放我們所定義的 struct 的內(nèi)存之前調(diào)用 finalizers。
讓我們深入其中,看看在更多的 Allocation 中到底發(fā)生了些什么。
package main import ( "fmt" "math/rand" "runtime" "runtime/debug" "strconv" "time" ) type Foo struct { a int } func main() { debug.SetGCPercent(-1) var ms runtime.MemStats runtime.ReadMemStats(&ms) fmt.Printf("Allocation: %f Mb, Number of allocation: %d\n", float32(ms.HeapAlloc)/float32(1024*1204), ms.HeapObjects) for i := 0; i < 1000000; i++ { f := NewFoo(i) _ = fmt.Sprintf("%d", f.a) } runtime.ReadMemStats(&ms) fmt.Printf("Allocation: %f Mb, Number of allocation: %d\n", float32(ms.HeapAlloc)/float32(1024*1204), ms.HeapObjects) runtime.GC() time.Sleep(time.Second) runtime.ReadMemStats(&ms) fmt.Printf("Allocation: %f Mb, Number of allocation: %d\n", float32(ms.HeapAlloc)/float32(1024*1204), ms.HeapObjects) runtime.GC() time.Sleep(time.Second) } //go:noinline func NewFoo(i int) *Foo { f := &Foo{a: rand.Intn(50)} runtime.SetFinalizer(f, func(f *Foo) { _ = fmt.Sprintf("foo " + strconv.Itoa(i) + " has been garbage collected") }) return f }
一百萬個 structs 和 finalizers 被創(chuàng)建出來,下面是輸出:
Allocation: 0.090862 Mb, Number of allocation: 137 Allocation: 31.107506 Mb, Number of allocation: 2390078 Allocation: 110.052666 Mb, Number of allocation: 4472742
讓我們再試一次,這次不用 finalizers:
Allocation: 0.090694 Mb, Number of allocation: 136 Allocation: 18.129814 Mb, Number of allocation: 1390078 Allocation: 0.094451 Mb, Number of allocation: 154
看起來沒有任何資源在內(nèi)存中被清理掉,即使垃圾回收器被觸發(fā),且 finalizers 也運(yùn)行。為了理解這一行為,讓我們回到那篇關(guān)于 runtime 的文檔:
當(dāng)垃圾回收器發(fā)現(xiàn)了一個已關(guān)聯(lián) finalizer 的無法訪問的塊,這說明了關(guān)聯(lián)操作與運(yùn)行 finalizer 是在一個單獨(dú)的 gorountine 下。這讓 obj 再次可訪問,不過現(xiàn)在沒有了一個關(guān)聯(lián)的 finalizer,假設(shè) SetFinalizer 沒有再次被調(diào)用,當(dāng)下次垃圾回收器看到這個 obj 時,它是不可被訪問的,并將回收它。
如我們所見,finalizers 首先會被移除,然后內(nèi)存將在下一次循環(huán)中被釋放,讓我們再次運(yùn)行第一個例子,并加上兩個強(qiáng)制的垃圾回收操作。
Allocation: 0.090862 Mb, Number of allocation: 137 Allocation: 31.107506 Mb, Number of allocation: 2390078 Allocation: 110.052666 Mb, Number of allocation: 4472742 Allocation: 0.099220 Mb, Number of allocation: 166
我們可以清楚地看到,第二次運(yùn)行將會清理數(shù)據(jù),finalizers 最終也對性能和內(nèi)存使用產(chǎn)生了輕微的作用。
性能表現(xiàn)
下文闡述了為何 finalizers 逐個運(yùn)行:
一個單獨(dú) goroutine 為了一個程序運(yùn)行了所有的 finalizers,然而,如果一個 finalizer 必須長時間運(yùn)行,則需要開啟一個新的 gorountine。
僅一個 goroutine 將會運(yùn)行 finalizers,并且任何超重任務(wù)都需要開啟一個新的 gorountine。當(dāng) finalizers 運(yùn)行時,垃圾回收器并沒有停止且并發(fā)運(yùn)行中。因此 finalizer 并不該影響你的應(yīng)用的性能表現(xiàn)。
同時,一旦 finalizer 不再被需要,Go 提供了一個方法來移除它。
runtime.SetFinalizer(p, nil)
它允許我們根據(jù)使用情況動態(tài)地移除 finalizers。
應(yīng)用中的使用
內(nèi)部上,Go 在 net 以及 net/http 包中確保文件先前的打開與關(guān)閉準(zhǔn)確無誤,并且在 os 包中確保之前創(chuàng)建的進(jìn)程被正常地釋放。這里有一個來自 os 包的例子:
func newProcess(pid int, handle uintptr) *Process { p := &Process{Pid: pid, handle: handle} runtime.SetFinalizer(p, (*Process).Release) return p }
當(dāng)這個進(jìn)程被釋放,finalizer 也會被移除。
func (p *Process) release() error { // NOOP for unix. p.Pid = -1 // no need for a finalizer anymore runtime.SetFinalizer(p, nil) return nil }
Go 同樣也在測試中使用 finalizers 確保在垃圾回收器中期望的動作被執(zhí)行,舉個例子,sync 包使用了 finalizers 測試在垃圾回收循環(huán)中 pool 是否被清空。
掃碼二維碼 獲取免費(fèi)視頻學(xué)習(xí)資料
- 本文固定鏈接: http://www.wangchenghua.com/post/7238/
- 轉(zhuǎn)載請注明:轉(zhuǎn)載必須在正文中標(biāo)注并保留原文鏈接
- 掃碼: 掃上方二維碼獲取免費(fèi)視頻資料