Go 最細節篇|記憶體回收又踩坑了
分享一個 GC 相關的踩坑實踐。公司線上某元件記憶體資源洩漏,偶發 oom 。透過 Go 的 pprof 排查,很快速定位到洩漏的資料結構 A ,結構 A 的相關資源是透過 Go 的 Finalizer 機制來釋放的。但詭異的來了,對照著程式碼審視了多次之後,大家一致斷定,這段程式碼絕對沒有洩漏的問題。但是,事實勝於雄辯,現實就是洩漏就在此處。想不通。。。
幾天之後,問題的轉機來自於另一個毫不相關的地方,我們發現了一個卡住的協程。最開始並不在意,因為雖然卡住是異常的,但是洩漏的地點差了十萬八千里,兩者毫不相關。所以剛開始是忽略的。
後來實在是想不開,閒來無事,把這個異常點拿來看,才發現一點點線索。這個卡住的協程是一個結構體 B 的釋放過程,和 A 一樣也是 Go 的 Finalizer 機制。我們踩的坑就於此有關,很典型,出人意料,所以分享給大家。先複習一下 Finalizer 機制。
什麼是 Go 的 Finalizer 機制?
那麼什麼是 Finalizer 機制呢?這個就必須要再提一嘴 Go 的 GC 機制了。這個是 Go 比較有特色的機制。在 Go 里程式設計師負責申請記憶體,Go 的 runtime 的 GC 機制負責回收。
在這個過程,Go 語言還提供了一個 Finalizer 機制,允許程式設計師在申請的時候指定一個回撥函式,在 GC 回收到這個結構體記憶體的時候,Go 會自動呼叫一次這個回撥函式。
func SetFinalizer(obj interface{}, finalizer interface{})
這個非常實用的一個技巧,在文章《程式設計思考:物件生命週期的問題》裡有分享。主要是比較安全的解決掉物件宣告週期的問題。因為程式設計師自己來管理資源的釋放,那很可能出 bug ,比如在有人用的時候呼叫釋放。透過 Finalizer 機制,則能保證一定是無人引用的結構體記憶體,才會執行回撥。
舉個例子:
type TestStruct struct {
name string
}
//go:noinline
func newTestStruct() *TestStruct {
v := &TestStruct{"n1"}
runtime.SetFinalizer(v, func(p *TestStruct) {
fmt.Println("gc Finalizer")
})
return v
}
func main() {
t := newTestStruct()
fmt.Println("== start ===")
_ = t
fmt.Println("== ... ===")
runtime.GC()
fmt.Println("== end ===")
}
上面的例子,給結構體 TestStruct 的釋放設定了一個 Finalizer 回撥函式。然後在主動呼叫 runtime.GC 來快速回收,童鞋可以體驗一下。
Finalizer 這裡竟然有個坑?
Finalizer 很好用這是事實,但 Finalizer 機制也有限制條件,在官網上有如下宣告:
A single goroutine runs all finalizers for a program, sequentially. If a finalizer must run for a long time, it should do so by starting a new goroutine.
來自 ,什麼意思?
說得是,Go 的 runtime 是用一個單 goroutine 來執行所有的 Finalizer 回撥,還是序列化的。
劃重點:一旦執行某個 Finalizer 出了問題,可能會影響到全域性的 Finalizer 回撥函式的執行。
原來如此!!
我們這次就是精準踩坑。在釋放 B 結構體的時候,呼叫了一個 Finalizer 回撥,然後把協程卡死了。導致後續所有的 Finalizer 回撥都執行不了,比如 A 的 Finalizer 就無法執行,從而導致資源的洩漏和各種的異常。
舉個例子:
var (
done chan struct{}
)
type A struct {
name string
}
type B struct {
name string
}
type C struct {
name string
}
func newA() *A {
v := &A{"n1"}
runtime.SetFinalizer(v, func(p *A) {
fmt.Println("gc Finalizer A")
})
return v
}
func newB() *B {
v := &B{"n1"}
runtime.SetFinalizer(v, func(p *B) {
<-done
fmt.Println("gc Finalizer B")
})
return v
}
func newC() *C {
v := &C{"n1"}
runtime.SetFinalizer(v, func(p *C) {
fmt.Println("gc Finalizer C")
})
return v
}
func main() {
a := newA()
b := newB()
c := newC()
fmt.Println("== start ===")
_, _, _ = a, b, c
fmt.Println("== ... ===")
for i := 0; i < 10; i++ {
runtime.GC()
}
fmt.Println("== end ===")
}
這裡建立了一個極簡的例子,A,B, C 例項都設定了 Finalizer 回撥,故意讓其中一個阻塞住,會影響到剩下的 Finalizer 的執行。
總結
Go 提供的 Finalizer 機制,讓程式設計師建立的時候註冊回撥函式,能很好的幫助程式設計師解決資源安全釋放的問題; Finalizer 的執行是全域性單協程,且序列化執行的。所以可能會因為某一次的卡住導致全域性的失效,切記; 排查記憶體問題的時候,pprof 看現場很明確,但是根因可能是看似毫不相關的旮旯角落,有時候要把思維跳出來排查;
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2938249/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 又踩坑了!BigDecimal使用的5個坑!Decimal
- Go踩坑筆記(十九)Go筆記
- Go json 踩坑記錄GoJSON
- Go 開發時要了解的 1 個記憶體模型細節Go記憶體模型
- 垃圾回收與記憶體分配——總結篇記憶體
- 【JVM之記憶體與垃圾回收篇】堆JVM記憶體
- redis的記憶體滿了之後,redis如何回收記憶體嗎Redis記憶體
- SQL Server 在AlwaysOn上使用記憶體表"踩坑"SQLServer記憶體
- 【面試篇】Go語言常見踩坑(一)面試Go
- 記憶體回收介紹記憶體
- dubbo-go v3 版本 go module 踩坑記Go
- [踩坑] Go Modules 使用Go
- JavaScript之坑了我--事件模型細節JavaScript事件模型
- JavaScript之坑了我--函式細節JavaScript函式
- Go 高效能系列教程之五:記憶體和垃圾回收Go記憶體
- js記憶體回收機制JS記憶體
- .NET記憶體管理、垃圾回收記憶體
- java記憶體垃圾回收模型Java記憶體模型
- 解讀SQL 記憶體資料庫的細節SQL記憶體資料庫
- Go:記憶體管理與記憶體清理Go記憶體
- JVM記憶體回收機制——哪些記憶體需要被回收(JVM學習系列2)JVM記憶體
- JVM垃圾回收器、記憶體分配與回收策略JVM記憶體
- 【JVM之記憶體與垃圾回收篇】虛擬機器棧JVM記憶體虛擬機
- 【JVM之記憶體與垃圾回收篇】物件例項化記憶體佈局與訪問定位JVM記憶體物件
- 【JVM之記憶體與垃圾回收篇】JVM與Java體系結構JVM記憶體Java
- removeChild踩坑記REM
- mpVue 踩坑記Vue
- vue 踩坑記Vue
- vuepress踩坑記Vue
- IE 踩坑記
- Redis記憶體——記憶體消耗(記憶體都去哪了?)Redis記憶體
- Node - 記憶體管理和垃圾回收記憶體
- Node記憶體限制和垃圾回收記憶體
- Java記憶體管理 -JVM 垃圾回收Java記憶體JVM
- Java堆外直接記憶體回收Java記憶體
- JVM記憶體管理和垃圾回收JVM記憶體
- JavaScript 記憶體管理及垃圾回收JavaScript記憶體
- linux記憶體回收機制Linux記憶體