golang中GC的Q&A
阿新 • • 發佈:2022-03-16
GC的認識 Q&A
什麼是GC,有什麼作用?
- GC全稱garbage collection, 即垃圾回收,是一種自動記憶體管理的機制
- 當程式向作業系統申請的記憶體不在需要時,垃圾回收主動將其回收並供其它程式碼申請記憶體時複用,或者歸還給作業系統
這種針對記憶體級別資源的自動回收過程,稱為垃圾回收,而負責垃圾回收的元件,稱為垃圾回收器
根物件到底是什麼
- 根物件在垃圾回收的術語中又叫根集合,它是垃圾回收器在標記過程時最先檢查的物件包括
- 全域性變數:程式在編譯器就能確定的那些存在於程式整個生命週期的變數
- 執行棧:每個goroutine都包含自己的執行棧,這些執行棧上包含棧上的變數以及指向分配的堆記憶體塊的指標
- 暫存器:暫存器的值可能表示一個指標,參與計算的這些指標可能指向某些賦值器分配的堆記憶體區塊
常見的GC實現方式有哪些,go語言的GC使用的是什麼?
- 所有GC演算法存在的形式可以歸結為tracing追蹤,reference counting引用計數,這兩種形式的混合使用
- tracing追蹤式GC
從根物件出發,根據物件之間的引用資訊,一步步推進直到掃描完整個堆並確定並確定要保留的物件,從而回收所有
可回收的物件,Go,java,v8對javascript的實現等均未追蹤式GC - 引用計數式GC
每個物件自身包含一個被引用的計數器,當計數器歸零時自動得到回收,因為此方法缺陷較多,在追求高效能時
通常不被使用,python,objective-C是引用式計數GC
- 目前常見的GC實現方式包括
- 追蹤式,分為多種不同型別,例如:
- 標記清掃:從根物件出發,將確定存活的物件進行標記,並清掃可以回收的物件
- 標記整理:為了解決記憶體碎片問題而提出,在標記過程中,將物件儘可能整理到一塊連續的記憶體上
- 增量式:將標記與清掃的過程分配執行,每次執行很小的部分,從而增量的推進垃圾回收,達到近似實時,
幾乎無停頓的目的 - 增量整理:在增量式的基礎上,增加對物件的整理過程
- 分代式:將物件根據存活時間的長短進行分類,存活時間小於某個值的為年輕代,存活時間大於某個值的為老年代
永遠不會參與回收的為永久代,並根據分代假設(如果一個物件存活時間不長則傾向於被回收,如果一個物件已經存活
很長時間了則傾向於存活更長時間)對 物件進行回收
- 分代式:將物件根據存活時間的長短進行分類,存活時間小於某個值的為年輕代,存活時間大於某個值的為老年代
- 引用計數
根據物件自身的引用計數來回收,當引用計數為0時立即回收
- Go 的 GC 目前使用的是無分代(物件沒有代際之分)、不整理(回收過程中不對物件進行移動與整理)、
併發(與使用者程式碼併發執行)的三色標記清掃演算法。原因[1]在於:
- 物件整理的優勢是解決記憶體碎片問題以及“允許”使用順序記憶體分配器。但 Go 執行時的分配演算法基於 tcmalloc,基本上沒有碎片問題。
並且順序記憶體分配器在多執行緒的場景下並不適用。Go 使用的是基於 tcmalloc 的現代記憶體分配演算法,
對物件進行整理不會帶來實質性的效能提升。 - 分代 GC 依賴分代假設,即 GC 將主要的回收目標放在新建立的物件上(存活時間短,更傾向於被回收),而非頻繁檢查所有物件。
- 但 Go 的編譯器會通過逃逸分析將大部分新生物件儲存在棧上(棧直接被回收),
- 只有那些需要長期存在的物件才會被分配到需要進行垃圾回收的堆中。也就是說,分代 GC 回收的那些存活時間短的物件在
- Go 中是直接被分配到棧上,當 goroutine 死亡後棧也會被直接回收,不需要 GC 的參與,進而分代假設並沒有帶來直接優勢。
- 並且go的垃圾回收器與使用者程式碼併發執行,使得 STW 的時間與物件的代際、物件的 size 沒有關係。
- Go 團隊更關注於如何更好地讓 GC 與使用者程式碼併發執行(使用適當的 CPU 來執行垃圾回收),而非減少停頓時間這一單一目標上。
三色標記法是什麼
- go語言中採用三色標記法進行gc垃圾回收
- 當我們談到三色標記法時其實就是指標記清掃的垃圾回收,三色標記法的作用就是用邏輯上嚴密推導標記清理這種垃圾回收法的正確性
STW是什麼意思
如何觀察GO GC
有了GC 為什麼還會發生記憶體洩漏
- 在一個具有GC的語言中,我們常說的記憶體洩漏,用嚴謹的話說應該是,預期能很快被釋放的記憶體附著在了長期存活的記憶體上,
或生命期以外的被延長,導致預計能回收的記憶體長時間得不到回收 - 在go中,由於goroutine的存在,所謂的記憶體洩漏除了附著在長期的物件上以外,還存在多種不同的形式
- 形式1 預期能被迅速釋放的記憶體由於被根物件引用而沒有被快速釋放
當有一個全域性物件,可能不經意間某個變數附著在其上時,且忽略了將其進行釋放,則該記憶體永遠不會得到釋放,例如
var cache = map[interface{}]interface{}{}
func keepAlloc() {
for i := 0; i < 10000; i++{
m := make([]byte, 2<<10)
cache[i] = m
}
}
- 形式2:goroutine洩漏
func keepalloc2() {
for i := 0; i < 100000; i++ {
go func() {
select {}
}()
}
}
- 形式3: channel導致goroutine洩漏
var ch = make(chan int)
func keepalloc3() {
for i := 0; i < 5; i++ {
// 沒有接收方,goroutine 會一直阻塞
// 但是由於沒有接收方對ch進行接收,所以會直接panic
go func() { ch <- 11 }()
}
}
func main() {
keepalloc3()
select {
}
}