原文http://alblue.cn/articles/2020/07/07/1594131614114.html#toc_h4_19
GC(garbage cycle)垃圾回收機(jī)制,是用于對(duì)申請(qǐng)的內(nèi)存進(jìn)行回收,防止內(nèi)存泄露等問題的一種機(jī)制。
go的GC機(jī)制
| 調(diào)用方式 | 所在位置 | 代碼 |
|---|---|---|
| 定時(shí)調(diào)用 | runtime/proc.go:forcegchelper() | gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) |
| 分配內(nèi)測(cè)時(shí)調(diào)用 | runtime/malloc.go:mallocgc() | gcTrigger{kind: gcTriggerHeap} |
| 手動(dòng)調(diào)用 | runtime/mgc.go:GC() | gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) |
三色標(biāo)記法
以下是Golang GC算法的里程碑:
- v1.1 STW
- v1.3 Mark STW, Sweep 并行
- v1.5 三色標(biāo)記法
- v1.8 hybrid write barrier(混合寫屏障)
go的gc是基于 標(biāo)記-清掃算法,并做了一定改進(jìn),減少了STW的時(shí)間。
標(biāo)記-清掃(Mark And Sweep)算法
此算法主要有兩個(gè)主要的步驟:
- 標(biāo)記(Mark phase)
- 清除(Sweep phase)
第一步,找出不可達(dá)的對(duì)象,然后做上標(biāo)記。
第二步,回收標(biāo)記好的對(duì)象。
操作非常簡單,但是有一點(diǎn)需要額外注意:mark and sweep算法在執(zhí)行的時(shí)候,需要程序暫停!即 stop the world。
標(biāo)記-清掃(Mark And Sweep)算法存在什么問題?
標(biāo)記-清掃(Mark And Sweep)算法這種算法雖然非常的簡單,但是還存在一些問題:
- STW,stop the world;讓程序暫停,程序出現(xiàn)卡頓。
- 標(biāo)記需要掃描整個(gè)heap
- 清除數(shù)據(jù)會(huì)產(chǎn)生heap碎片
這里面最重要的問題就是:mark-and-sweep 算法會(huì)暫停整個(gè)程序。
三色并發(fā)標(biāo)記法
1.首先將程序創(chuàng)建的對(duì)象全部標(biāo)記為白色
2.gc開始掃描,并將可達(dá)的對(duì)象標(biāo)記為灰色
3.再從灰色對(duì)象中找到其引用的對(duì)象,將其標(biāo)記為灰色,將自身標(biāo)記成黑色
重復(fù)以上2、3步驟,直至沒有灰色對(duì)象
4.對(duì)所有白色對(duì)象進(jìn)行清除
gc和用戶邏輯如何并行操作?
標(biāo)記-清除(mark and sweep)算法的STW(stop the world)操作,就是runtime把所有的線程全部凍結(jié)掉,所有的線程全部凍結(jié)意味著用戶邏輯是暫停的。這樣所有的對(duì)象都不會(huì)被修改了,這時(shí)候去掃描是絕對(duì)安全的。
Go如何減短這個(gè)過程呢?標(biāo)記-清除(mark and sweep)算法包含兩部分邏輯:標(biāo)記和清除。
我們知道Golang三色標(biāo)記法中最后只剩下的黑白兩種對(duì)象,黑色對(duì)象是程序恢復(fù)后接著使用的對(duì)象,如果不碰觸黑色對(duì)象,只清除白色的對(duì)象,肯定不會(huì)影響程序邏輯。所以:清除操作和用戶邏輯可以并發(fā)。
進(jìn)程新生成對(duì)象的時(shí)候,GC該如何操作呢?不會(huì)亂嗎?
Golang為了解決這個(gè)問題,引入了 寫屏障這個(gè)機(jī)制。
寫屏障:該屏障之前的寫操作和之后的寫操作相比,先被系統(tǒng)其它組件感知。
通俗的講:就是在gc跑的過程中,可以監(jiān)控對(duì)象的內(nèi)存修改,并對(duì)對(duì)象進(jìn)行重新標(biāo)記。(實(shí)際上也是超短暫的stw,然后對(duì)對(duì)象進(jìn)行標(biāo)記)
在上述情況中,新生成的對(duì)象,一律都標(biāo)位灰色!
那么,灰色或者黑色對(duì)象的引用改為白色對(duì)象的時(shí)候,Golang是該如何操作的?
看如下圖,一個(gè)黑色對(duì)象引用了曾經(jīng)標(biāo)記的白色對(duì)象。
[圖片上傳失敗...(image-484f2d-1595036887912)]
這時(shí)候,寫屏障機(jī)制被觸發(fā),向GC發(fā)送信號(hào),GC重新掃描對(duì)象并標(biāo)位灰色。

因此,gc一旦開始,無論是創(chuàng)建對(duì)象還是對(duì)象的引用改變,都會(huì)先變?yōu)榛疑?/p>
堆棧
內(nèi)存分配中的堆和棧
棧(操作系統(tǒng)):由操作系統(tǒng)自動(dòng)分配釋放 ,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
堆(操作系統(tǒng)): 一般由程序員分配釋放, 若程序員不釋放,程序結(jié)束時(shí)可能由OS回收,分配方式倒是類似于鏈表。
堆棧緩存方式
棧使用的是一級(jí)緩存, 他們通常都是被調(diào)用時(shí)處于存儲(chǔ)空間中,調(diào)用完畢立即釋放。
堆則是存放在二級(jí)緩存中,生命周期由虛擬機(jī)的垃圾回收算法來決定(并不是一旦成為孤兒對(duì)象就能被回收)。所以調(diào)用這些對(duì)象的速度要相對(duì)來得低一些。
申請(qǐng)到 棧內(nèi)存 好處:函數(shù)返回直接釋放,不會(huì)引起垃圾回收,對(duì)性能沒有影響。
內(nèi)存分配逃逸
所謂逃逸分析(Escape analysis)是指由編譯器決定內(nèi)存分配的位置,不需要程序員指定。
在函數(shù)中申請(qǐng)一個(gè)新的對(duì)象:
- 如果分配 在棧中,則函數(shù)執(zhí)行結(jié)束可自動(dòng)將內(nèi)存回收;
- 如果分配在堆中,則函數(shù)執(zhí)行結(jié)束可交給GC(垃圾回收)處理;
逃逸場景(什么情況才分配到堆中)
指針逃逸
package main
type Student struct {
Name string
Age int
}
func StudentRegister(name string, age int) *Student {
s := new(Student) //局部變量s逃逸到堆
s.Name = name
s.Age = age
return s
}
func main() {
StudentRegister("Jim", 18)
}
雖然 在函數(shù) StudentRegister() 內(nèi)部 s 為局部變量,其值通過函數(shù)返回值返回,s 本身為一指針,其指向的內(nèi)存地址不會(huì)是棧而是堆,這就是典型的逃逸案例。
終端運(yùn)行命令查看逃逸分析日志:
<pre>go build -gcflags=-m</pre>
輸出
./main.go:16:6: can inline StudentRegister
./main.go:25:6: can inline main
./main.go:26:17: inlining call to StudentRegister
./main.go:16:22: leaking param: name
./main.go:17:10: new(Student) escapes to heap
./main.go:26:17: new(Student) does not escape
可見在StudentRegister()函數(shù)中,也即代碼第10行顯示”escapes to heap”,代表該行內(nèi)存分配發(fā)生了逃逸現(xiàn)象。
??臻g不足逃逸(空間開辟過大)
package main
func Slice() {
s := make([]int, 1000, 1000)
for index, _ := range s {
s[index] = index
}
}
func main() {
Slice()
}
上面代碼Slice()函數(shù)中分配了一個(gè)1000個(gè)長度的切片,是否逃逸取決于??臻g是否足夠大。 直接查看編譯提示,如下:
./main.go:20:6: can inline main
./main.go:13:11: make([]int, 1000, 1000) does not escape
所以只是1000的長度還不足以發(fā)生逃逸現(xiàn)象。然后就x10倍吧
./main.go:20:6: can inline main
./main.go:13:11: make([]int, 10000, 10000) escapes to heap
當(dāng)切片長度擴(kuò)大到10000時(shí)就會(huì)逃逸。
實(shí)際上當(dāng)??臻g不足以存放當(dāng)前對(duì)象時(shí)或無法判斷當(dāng)前切片長度時(shí)會(huì)將對(duì)象分配到堆中。
動(dòng)態(tài)類型逃逸(不確定長度大小)
很多函數(shù)參數(shù)為interface類型,比如fmt.Println(a …interface{}),編譯期間很難確定其參數(shù)的具體類型,也能產(chǎn)生逃逸。
如下代碼所示:
package main
import "fmt"
func main() {
s := "Escape"
fmt.Println(s)
}
又或者像前面提到的例子:
func F() {
a := make([]int, 0, 20) // 棧 空間小
b := make([]int, 0, 20000) // 堆 空間過大 逃逸
l := 20
c := make([]int, 0, l) // 堆 動(dòng)態(tài)分配不定空間 逃逸
}
閉包引用對(duì)象逃逸
Fibonacci數(shù)列的函數(shù):
package main
import "fmt"
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := Fibonacci()
for i := 0; i < 10; i++ {
fmt.Printf("Fibonacci: %d\n", f())
}
}
Fibonacci()函數(shù)中原本屬于局部變量的a和b由于閉包的引用,不得不將二者放到堆上,以致產(chǎn)生逃逸。
逃逸分析的作用是什么呢?
- 逃逸分析的好處是為了減少gc的壓力,不逃逸的對(duì)象分配在棧上,當(dāng)函數(shù)返回時(shí)就回收了資源,不需要gc標(biāo)記清除。
- 逃逸分析完后可以確定哪些變量可以分配在棧上,棧的分配比堆快,性能好(逃逸的局部變量會(huì)在堆上分配 ,而沒有發(fā)生逃逸的則有編譯器在棧上分配)。
- 同步消除,如果你定義的對(duì)象的方法上有同步鎖,但在運(yùn)行時(shí),卻只有一個(gè)線程在訪問,此時(shí)逃逸分析后的機(jī)器碼,會(huì)去掉同步鎖運(yùn)行。
逃逸總結(jié):
- 棧上分配內(nèi)存比在堆中分配內(nèi)存有更高的效率
- 棧上分配的內(nèi)存不需要GC處理
- 堆上分配的內(nèi)存使用完畢會(huì)交給GC處理
- 逃逸分析目的是決定內(nèi)分配地址是棧還是堆
- 逃逸分析在編譯階段完成
函數(shù)傳遞指針真的比傳值效率高嗎?
傳遞指針相比值傳遞減少了底層拷貝,可以提高效率,但是拷貝的數(shù)據(jù)量較小,由于指針傳遞會(huì)產(chǎn)生逃逸,可能會(huì)使用堆,也可能增加gc的負(fù)擔(dān),所以指針傳遞不一定是高效的。
GC的bug
https://zhuanlan.zhihu.com/p/32686933
代碼優(yōu)化
減少對(duì)象分配 所謂減少對(duì)象的分配,實(shí)際上是盡量做到,對(duì)象的重用。 比如像如下的兩個(gè)函數(shù)定義:
第一個(gè)函數(shù)沒有形參,每次調(diào)用的時(shí)候返回一個(gè) []byte,第二個(gè)函數(shù)在每次調(diào)用的時(shí)候,形參是一個(gè) buf []byte 類型的對(duì)象,之后返回讀入的 byte 的數(shù)目。
第一個(gè)函數(shù)在每次調(diào)用的時(shí)候都會(huì)分配一段空間,這會(huì)給 gc 造成額外的壓力。第二個(gè)函數(shù)在每次迪調(diào)用的時(shí)候,會(huì)重用形參聲明。
老生常談 string 與 []byte 轉(zhuǎn)化 在 stirng 與 []byte 之間進(jìn)行轉(zhuǎn)換,會(huì)給 gc 造成壓力 通過 gdb,可以先對(duì)比下兩者的數(shù)據(jù)結(jié)構(gòu):
兩者發(fā)生轉(zhuǎn)換的時(shí)候,底層數(shù)據(jù)結(jié)結(jié)構(gòu)會(huì)進(jìn)行復(fù)制,因此導(dǎo)致 gc 效率會(huì)變低。解決策略上,一種方式是一直使用 []byte,特別是在數(shù)據(jù)傳輸方面,[]byte 中也包含著許多 string 會(huì)常用到的有效的操作。另一種是使用更為底層的操作直接進(jìn)行轉(zhuǎn)化,避免復(fù)制行為的發(fā)生。
少量使用+連接 string 由于采用 + 來進(jìn)行 string 的連接會(huì)生成新的對(duì)象,降低 gc 的效率,好的方式是通過 append 函數(shù)來進(jìn)行。
append操作 在使用了append操作之后,數(shù)組的空間由1024增長到了1312,所以如果能提前知道數(shù)組的長度的話,最好在最初分配空間的時(shí)候就做好空間規(guī)劃操作,會(huì)增加一些代碼管理的成本,同時(shí)也會(huì)降低gc的壓力,提升代碼的效率。
https://www.cnblogs.com/maoqide/p/12355565.html
三色標(biāo)記法+混合寫屏障