簡介
每個版本的Golang的垃圾回收都在不斷優化中,而且方法和策略都在變化,因此這裏只是總結出以下幾個關鍵點:
- 什麼樣的數據需要GC
- 觸發GC的條件是什麼
- GC時發生了什麼
- 能否從代碼層面上提高GC的效率
GC的基本流程
Golang在確定的時間,或者內存分配到達一定程度時,進行GC。GC時,會停止STW(Stop The World),即對外的服務都會暫停,然後進行垃圾回收處理。Go1.12引入了三色標記法和write-barrier的方式;在Go1.14中,引入看了搶佔式回收機制。
write-barrier機制:
假設有4個G在運行,如下圖:
進行GC的時候,需要STW,此時的4個G都要停止工作。如果有一個沒有停止工作的,則GC暫時不能發生。比如下圖:
第4個G沒停止工作,則GC需要等待其結束。Go1.14中,可以搶佔第4個G的工作狀態,保存其狀態後,再進行GC
GC的時候,GC機制會徵用一些G併發輔助進行工作,一般有25%的G會被徵用。
整體工作流程:
- 創建白、灰和黑三個集合
- 初始化所有的待回收對象都是白色的
- 從根節點遍歷對象,不遞歸;遍歷到的白色對象放到灰色集合當中
- 之後遍歷灰色集合,把灰色對象引用的對象,從白色集合中放入灰色集合,並把現在的灰色對象放入黑色集合中
- 重複上一步,知道灰色集合是空的
- 通過write-barrier檢測對象的變化,重複以上操作
- 回收所有的白色對象
GC回收的對象
永遠不要過早的優化程序!!!
棧內存分配和回收的代價遠遠小於堆內存。Golang的垃圾回收發生在全局的堆上和每個Goroutinue的棧上。回收棧內存只需要兩個CPU指令,push和pop。然而,分配在棧內存的數據,需要在編譯期間就得知道type和size。
Golang的編譯器使用“逃逸分析”的方式,決定採取棧回收還是堆回收的方式。如果發生逃逸,則使用堆回收。
go build -gcflags '-m'
命令可以分析內存逃逸的現象。
發生內存逃逸的幾種情況:
- 向
chan
中發送數據的指針或者包含指針的值。
因爲編譯器此時不知道值什麼時候會被接收,因此只能放入堆中 - 非直接的函數調用,比如在閉包中引用包外的值,因爲閉包執行的生命週期可能會超過函數週期,因此需要放入堆中
- 在slice或map中存儲指針或者包含指針的值。
比如[]*string
,即使slice是在棧上,其存儲的值仍然會放入堆中 - slice如果底層使用array作爲容器,在使用append擴容的時候。但是,如果知道具體擴容的數量,則仍然在棧上。
如果在編譯期間,slice知道自己的size,那麼放入棧中。更多的時候,是不知道size的,比如append的時候,此時只能放入堆中。 interface
類型多態的時候調用方法,此時會發生逃逸- 指針、slice和map作爲參數返回的時候,此時肯定要發生逃逸。
總結一下發生逃逸的結論:
- 首先明確一點,Golang中所有的數據都是按值傳遞,這點和C語言是一樣的(注意Golang中的數組名是值,和C的差別)。所謂的map、slice和chan等是引用,其本質原因是,這些結構的內部都有指針,複製的時候,內部都是複製的指針,因此表現的是傳值。
- 在函數調用中,對於指針的情況,只要指向的地址的所有者只有一個,那麼必然是棧回收;而一旦存在地址存在不確定變化時,則轉換成堆的數據。比如slice情況,因爲slice會擴容或者縮容,因此造成不確定情況。
以下使用代碼示例說明:
package main
func main() {
ch := make(chan int, 1)
x := 5
ch <- x // x不發生逃逸,因爲只是複製的值
ch1 := make(chan *int, 1)
y := 5
py := &y
ch1 <- py // y逃逸,因爲地址傳入了chan
z := 5
pz := &z // z不逃逸,因爲是確定性析構
*pz += 1
}
執行命令:go build -gcflags ./main.go
,得到結論是y z都發生了逃逸。
# command-line-arguments
.\main.go:3:6: can inline main as: func() { ch := make(chan int, 1); x := 5; ch <- x; ch1 := make(chan *int, 1); y := 5; py := &y; ch1 <- py; z := 5; pz := &z; *pz += 1 }
.\main.go:9:2: y escapes to heap:
.\main.go:9:2: flow: py = &y:
.\main.go:9:2: from &y (address-of) at .\main.go:10:8
.\main.go:9:2: from py := &y (assign) at .\main.go:10:5
.\main.go:9:2: flow: {heap} = py:
.\main.go:9:2: from ch1 <- py (send) at .\main.go:11:6
.\main.go:9:2: moved to heap: y
如果使用slice和map的模式:
package main
func main() {
var x int
x = 10
var ls []*int
ls = append(ls, &x)
var y int
var mp map[string]*int
mp["y"] = &y
}
結論分析:
# command-line-arguments
.\main.go:3:6: can inline main as: func() { var x int; x = <N>; x = 10; var ls []*int; ls = <N>; ls = append(ls, &x); var y int; y = <N>; var mp map[string]*int; mp = <N>; mp["y"] = &y }
.\main.go:4:6: x escapes to heap:
.\main.go:4:6: flow: {heap} = &x:
.\main.go:4:6: from &x (address-of) at .\main.go:7:18
.\main.go:4:6: from append(ls, &x) (call parameter) at .\main.go:7:13
.\main.go:9:6: y escapes to heap:
.\main.go:9:6: flow: {heap} = &y:
.\main.go:9:6: from &y (address-of) at .\main.go:11:12
.\main.go:9:6: from mp["y"] = &y (assign) at .\main.go:11:10
.\main.go:4:6: moved to heap: x
.\main.go:9:6: moved to heap: y
使用閉包捕獲指針的模式:
package main
import "time"
func main() {
x := 10
go func(x *int) {
*x += 1
}(&x) // 捕獲的瞬間,x沒有移動到heap上,但是整個閉包移動到了heap上,因此x也跟隨閉包被移動到heap上了
time.Sleep(time.Second * 2)
}
結論分析:
# command-line-arguments
.\main.go:5:6: cannot inline main: unhandled op GO
.\main.go:7:5: can inline main.func1 as: func(*int) { *x += 1 }
.\main.go:7:5: func literal escapes to heap:
.\main.go:7:5: flow: {heap} = &{storage for func literal}:
.\main.go:7:5: from func literal (spill) at .\main.go:7:5
.\main.go:7:5: from go (func literal)(&x) (call parameter) at .\main.go:7:2
.\main.go:6:2: x escapes to heap:
.\main.go:6:2: flow: {heap} = &x:
.\main.go:6:2: from &x (address-of) at .\main.go:9:4
.\main.go:6:2: from go (func literal)(&x) (call parameter) at .\main.go:7:2
.\main.go:7:10: x does not escape
.\main.go:6:2: moved to heap: x
.\main.go:7:5: func literal escapes to heap
對於slice擴容的情況:
package main
import (
"os"
"strconv"
)
func main() {
ls := []int{1, 2, 3}
ls = append(ls, 4) // 確定性的,不逃逸,編譯期間可以知道
var n int
n, _ = strconv.Atoi(os.Args[1]) // 輸入數據後,則結果不可知,因此可能逃逸
ls1 := []int{1, 2, 3}
for i := 0; i < n; i++ {
ls1 = append(ls1, 1)
}
}
interface類型的GC,涉及使用interface類型轉換並調用對應的方法的時候,都會發生內存逃逸,給出代碼示例:
package main
type foo interface {
fooFunc()
}
type foo1 struct{}
func (f1 foo1) fooFunc() {}
type foo2 struct{}
func (f2 *foo2) fooFunc() {}
func main() {
var f foo
f = foo1{}
f.fooFunc() // 調用方法時,發生逃逸,因爲方法是動態分配的
f = &foo2{}
f.fooFunc()
}
執行說明:
go-code ➤ go build -gcflags "-m" main.go
# command-line-arguments
.\main.go:9:6: can inline foo1.fooFunc
.\main.go:13:6: can inline (*foo2).fooFunc
.\main.go:13:7: f2 does not escape
.\main.go:17:4: foo1 literal escapes to heap
.\main.go:19:6: &foo2 literal escapes to heap
<autogenerated>:1: leaking param: .this
<autogenerated>:1: inlining call to foo1.fooFunc
<autogenerated>:1: .this does not escape
返回slice等的情況:
package main
func foo() []int {
return []int{1, 2, 3}
}
func main() {
ls := foo() // 發生逃逸
ls = append(ls, 1)
}
分析結果:
# command-line-arguments
.\main.go:3:6: can inline foo as: func() []int { return []int literal }
.\main.go:7:6: can inline main as: func() { ls := foo(); ls = append(ls, 1) }
.\main.go:8:11: inlining call to foo func() []int { return []int literal }
.\main.go:4:14: []int literal escapes to heap:
.\main.go:4:14: flow: ~r0 = &{storage for []int literal}:
.\main.go:4:14: from []int literal (spill) at .\main.go:4:14
.\main.go:4:14: from return []int literal (return) at .\main.go:4:2
.\main.go:4:14: []int literal escapes to heap
.\main.go:8:11: []int literal does not escape
傳值還是傳指針的問題:
根據上面的分析,指針更容易出現內存逃逸的現象。而一旦發生了內存逃逸,則不可避免地對GC造成潛在的壓力。有種錯誤的觀念:傳指針的代價總是比傳值的拷貝代價小。這種觀念只在像C語言這種沒有GC的低級語言中可能適用。原因如下:
- 對指針解引用的時候,編譯器會進行一些檢查。
- 指針一般都不是臨近地址的引用,而複製時,一般都是CPU cash中的數據,cash line內的數據的複製,速度基本和一個複製指針相等
因此,對於小型的數據,一般傳值就夠了。在某些情況下,需要對代碼做一些重構,以消除成員變量中不必要的指針類型。slice有些情況下,可能也會造成內存逃逸,使用已知固定長度的slice,某些情況下會減少內存逃逸。nterface
調用方法會發生內存逃逸,某些熱點的情況下,可以考慮優化interface
的情況。
幾個總結
- 永遠不要過早的優化,使用數據驅動優化代碼
- 棧回收的代價遠遠小於堆回收的代價
- 指針一般情況下會影響棧回收
- 在熱點代碼片段,謹慎的使用
interface
參考文檔
- https://juejin.im/post/5d2825bff265da1b6836e8d4
- https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/
- https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
- https://i6448038.github.io/2019/03/04/golang-garbage-collector/
- http://idiotsky.top/2017/08/16/gc-three-color/