golang 的GC原理

原文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)生逃逸。

逃逸分析的作用是什么呢?

  1. 逃逸分析的好處是為了減少gc的壓力,不逃逸的對(duì)象分配在棧上,當(dāng)函數(shù)返回時(shí)就回收了資源,不需要gc標(biāo)記清除。
  2. 逃逸分析完后可以確定哪些變量可以分配在棧上,棧的分配比堆快,性能好(逃逸的局部變量會(huì)在堆上分配 ,而沒有發(fā)生逃逸的則有編譯器在棧上分配)。
  3. 同步消除,如果你定義的對(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)記法+混合寫屏障

https://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651439356&idx=2&sn=264a3141ea9a4b29fe67ec06a17aeb99&chksm=80bb1e0eb7cc97181b81ae731d0d425dda1e9a8d503ff75f217a0d77bd9d0eb451555cb584a0&scene=21#wechat_redirect

Golang內(nèi)存分配逃逸分析

https://www.cnblogs.com/shijingxiang/articles/12200355.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容