golang 垃圾回收GC的深層原理


GC(garbage cycle)垃圾回收機制,是用於對申請的內存進行回收,防止內存泄露等問題的一種機制。

go的GC機制

| 調用方式 | 所在位置 | 代碼 |
| - | - | - |
| 定時調用 | runtime/proc.go:forcegchelper() | gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) |
| 分配內測時調用 | runtime/malloc.go:mallocgc() | gcTrigger{kind: gcTriggerHeap} |
| 手動調用 | runtime/mgc.go:GC() | gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) |

三色標記法

以下是Golang GC算法的里程碑:

  • v1.1 STW
  • v1.3 Mark STW, Sweep 並行
  • v1.5 三色標記法
  • v1.8 hybrid write barrier(混合寫屏障)

go的gc是基於 標記-清掃算法,並做了一定改進,減少了STW的時間。

標記-清掃(Mark And Sweep)算法

此算法主要有兩個主要的步驟:

  • 標記(Mark phase)
  • 清除(Sweep phase)

第一步,找出不可達的對象,然后做上標記。

第二步,回收標記好的對象。

操作非常簡單,但是有一點需要額外注意:mark and sweep算法在執行的時候,需要程序暫停!即 stop the world

標記-清掃(Mark And Sweep)算法存在什么問題?

標記-清掃(Mark And Sweep)算法這種算法雖然非常的簡單,但是還存在一些問題:

  • STW,stop the world;讓程序暫停,程序出現卡頓。
  • 標記需要掃描整個heap
  • 清除數據會產生heap碎片

這里面最重要的問題就是:mark-and-sweep 算法會暫停整個程序。

三色並發標記法

1.首先將程序創建的對象全部標記為白色

2.gc開始掃描,並將可達的對象標記為灰色

3.再從灰色對象中找到其引用的對象,將其標記為灰色,將自身標記成黑色

重復以上2、3步驟,直至沒有灰色對象

4.對所有白色對象進行清除

gc和用戶邏輯如何並行操作?

標記-清除(mark and sweep)算法的STW(stop the world)操作,就是runtime把所有的線程全部凍結掉,所有的線程全部凍結意味着用戶邏輯是暫停的。這樣所有的對象都不會被修改了,這時候去掃描是絕對安全的。

Go如何減短這個過程呢?標記-清除(mark and sweep)算法包含兩部分邏輯:標記和清除。

我們知道Golang三色標記法中最后只剩下的黑白兩種對象,黑色對象是程序恢復后接着使用的對象,如果不碰觸黑色對象,只清除白色的對象,肯定不會影響程序邏輯。所以:清除操作和用戶邏輯可以並發。

進程新生成對象的時候,GC該如何操作呢?不會亂嗎?

Golang為了解決這個問題,引入了 寫屏障這個機制。

寫屏障:該屏障之前的寫操作和之后的寫操作相比,先被系統其它組件感知。

通俗的講:就是在gc跑的過程中,可以監控對象的內存修改,並對對象進行重新標記。(實際上也是超短暫的stw,然后對對象進行標記)

在上述情況中,新生成的對象,一律都標位灰色!

那么,灰色或者黑色對象的引用改為白色對象的時候,Golang是該如何操作的?

看如下圖,一個黑色對象引用了曾經標記的白色對象。

這時候,寫屏障機制被觸發,向GC發送信號,GC重新掃描對象並標位灰色。

因此,gc一旦開始,無論是創建對象還是對象的引用改變,都會先變為灰色。

堆棧

內存分配中的堆和棧

棧(操作系統):由操作系統自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。

堆(操作系統): 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收,分配方式倒是類似於鏈表。

堆棧緩存方式

棧使用的是一級緩存, 他們通常都是被調用時處於存儲空間中,調用完畢立即釋放。

堆則是存放在二級緩存中,生命周期由虛擬機的垃圾回收算法來決定(並不是一旦成為孤兒對象就能被回收)。所以調用這些對象的速度要相對來得低一些。

申請到 棧內存 好處:函數返回直接釋放,不會引起垃圾回收,對性能沒有影響。

內存分配逃逸

所謂逃逸分析(Escape analysis)是指由編譯器決定內存分配的位置,不需要程序員指定。

在函數中申請一個新的對象:

  • 如果分配 在棧中,則函數執行結束可自動將內存回收;
  • 如果分配在堆中,則函數執行結束可交給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)
}

  

雖然 在函數 StudentRegister() 內部 s 為局部變量,其值通過函數返回值返回,s 本身為一指針,其指向的內存地址不會是棧而是堆,這就是典型的逃逸案例。

終端運行命令查看逃逸分析日志:

go build -gcflags=-m

輸出

./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()函數中,也即代碼第10行顯示”escapes to heap”,代表該行內存分配發生了逃逸現象。

棧空間不足逃逸(空間開辟過大)

package main

func Slice() {
    s := make([]int, 1000, 1000)

    for index, _ := range s {
        s[index] = index
    }
}

func main() {
    Slice()
}

  

上面代碼Slice()函數中分配了一個1000個長度的切片,是否逃逸取決於棧空間是否足夠大。 直接查看編譯提示,如下:

./main.go:20:6: can inline main ./main.go:13:11: make([]int, 1000, 1000) does not escape 

所以只是1000的長度還不足以發生逃逸現象。然后就x10倍吧

./main.go:20:6: can inline main ./main.go:13:11: make([]int, 10000, 10000) escapes to heap 

當切片長度擴大到10000時就會逃逸。

實際上當棧空間不足以存放當前對象時或無法判斷當前切片長度時會將對象分配到堆中。

動態類型逃逸(不確定長度大小)

很多函數參數為interface類型,比如fmt.Println(a …interface{}),編譯期間很難確定其參數的具體類型,也能產生逃逸。

如下代碼所示:

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) // 堆 動態分配不定空間 逃逸
}

  

閉包引用對象逃逸

Fibonacci數列的函數:

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()函數中原本屬於局部變量的a和b由於閉包的引用,不得不將二者放到堆上,以致產生逃逸。

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

  1. 逃逸分析的好處是為了減少gc的壓力,不逃逸的對象分配在棧上,當函數返回時就回收了資源,不需要gc標記清除。
  2. 逃逸分析完后可以確定哪些變量可以分配在棧上,棧的分配比堆快,性能好(逃逸的局部變量會在堆上分配 ,而沒有發生逃逸的則有編譯器在棧上分配)。
  3. 同步消除,如果你定義的對象的方法上有同步鎖,但在運行時,卻只有一個線程在訪問,此時逃逸分析后的機器碼,會去掉同步鎖運行。

逃逸總結:

  • 棧上分配內存比在堆中分配內存有更高的效率
  • 棧上分配的內存不需要GC處理
  • 堆上分配的內存使用完畢會交給GC處理
  • 逃逸分析目的是決定內分配地址是棧還是堆
  • 逃逸分析在編譯階段完成

函數傳遞指針真的比傳值效率高嗎?

傳遞指針相比值傳遞減少了底層拷貝,可以提高效率,但是拷貝的數據量較小,由於指針傳遞會產生逃逸,可能會使用堆,也可能增加gc的負擔,所以指針傳遞不一定是高效的。

GC的bug

https://zhuanlan.zhihu.com/p/32686933

golang密集場景下協程調度飢餓問題

http://xiaorui.cc/archives/5251

代碼優化

減少對象分配 所謂減少對象的分配,實際上是盡量做到,對象的重用。 比如像如下的兩個函數定義:

第一個函數沒有形參,每次調用的時候返回一個 []byte,第二個函數在每次調用的時候,形參是一個 buf []byte 類型的對象,之后返回讀入的 byte 的數目。

第一個函數在每次調用的時候都會分配一段空間,這會給 gc 造成額外的壓力。第二個函數在每次迪調用的時候,會重用形參聲明。

老生常談 string 與 []byte 轉化 在 stirng 與 []byte 之間進行轉換,會給 gc 造成壓力 通過 gdb,可以先對比下兩者的數據結構:

兩者發生轉換的時候,底層數據結結構會進行復制,因此導致 gc 效率會變低。解決策略上,一種方式是一直使用 []byte,特別是在數據傳輸方面,[]byte 中也包含着許多 string 會常用到的有效的操作。另一種是使用更為底層的操作直接進行轉化,避免復制行為的發生。

少量使用+連接 string 由於采用 + 來進行 string 的連接會生成新的對象,降低 gc 的效率,好的方式是通過 append 函數來進行。

append操作 在使用了append操作之后,數組的空間由1024增長到了1312,所以如果能提前知道數組的長度的話,最好在最初分配空間的時候就做好空間規划操作,會增加一些代碼管理的成本,同時也會降低gc的壓力,提升代碼的效率。


https://www.cnblogs.com/maoqide/p/12355565.html

三色標記法+混合寫屏障

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

Golang內存分配逃逸分析

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM