go 垃圾回收那些事兒

1 垃圾回收算法有哪些

1.1 引用計數

算法思想:每一個單元維護一個域,保存其它單元指向它的引用數量(相似有向圖的入度)。當引用數量爲 0 時,將其回收。引用計數是漸進式的,可以將內存管理的開銷分佈到整個程序之中。C++ 的 share_ptr 使用的就是引用計算方法。html

引用計數算法實現通常是把全部的單元放在一個單元池裏,好比相似 free list。這樣全部的單元就被串起來了,就能夠進行引用計數了。新分配的單元計數值被設置爲 1(注意不是 0,由於申請通常都說 ptr = new object 這種)。每次有一個指針被設爲指向該單元時,該單元的計數值加 1;而每次刪除某個指向它的指針時,它的計數值減 1。當其引用計數爲 0 的時候,該單元會被進行回收。雖然這裏說的比較簡單,實現的時候仍是有不少細節須要考慮,好比刪除某個單元的時候,那麼它指向的全部單元都須要對引用計數減 1。java

優勢golang

  • 漸進式。內存管理與用戶程序的執行交織在一塊兒,將 GC 的代價分散到整個程序。不像標記-清掃算法須要 STW (Stop The World,GC 的時候掛起用戶程序)。
  • 算法簡單。
  • 內存回收快。因爲是跟用戶程序的執行交織在一塊兒,遇到引用計數爲0的對象就可當即回收,所以能夠快速回收內存,不像其餘的垃圾回收算法,須要等到內存耗盡或者達到某個閾值才進行垃圾回收。

缺點:web

  • 不能處理循環引用。大概這是被詬病最多的缺點了。不過針對這個問題,也除了不少解決方案,好比強引用等
  • 下降運行效率。內存單元的更新刪除等都須要維護相關的內存單元的引用計數,下降了程序運行效率。

1.2 標記清除

算法思想:把堆棧上的本地變量和任意靜態變量叫作根(roots),該算法分紅兩個階段,第一個階段是標記。遍歷全部的根變量,而且標記它,而且遞歸標記它所引用的變量。第二個階段,遍歷全部對象,沒有被標記的對象能夠回收內存,被標記過的對象去掉標記,方便下次從新標記。 第一階段僞代碼算法

func mark(objectP) {
	if !objectP.marked {
		objectP.marked = true
		for p := range objectP.refedObjs {
			mark(p)
		}
	}
}
複製代碼

第二階段僞代碼segmentfault

func sweep() {
	for p range in the heap {
		if p.marked{
			p.marked = false
		}else{
			head.release(p)
		}
	}
}
複製代碼

由於標記清除式的垃圾回收跟蹤了由根(root)訪問的全部對象,因此即便是在有循環引用時,它也能夠正確的標記並執行垃圾回收工做,這是標記清除最大的優點,而且對對象不會有額外的內存開銷和維護。數組

缺點是垃圾回收的時候須要stop the word,影響用戶程序的運行。markdown

1.3 節點複製

也叫作複製算法(copying算法)。一開始就會將可用內存分爲兩塊,from域和to域, 每次只是使用from域,to域則空閒着。當from域內存不夠了,開始執行GC操做,這個時候,會把from域存活的對象拷貝到to域,而後直接把from域進行內存清理。併發

jvm將Heap 內存劃分爲新生代與老年代,又將新生代劃分爲Eden(伊甸園) 與2塊Survivor Space(倖存者區) ,而後在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實行Copying 算法。 不過jvm在應用coping算法時,並非把內存按照1:1來劃分的,這樣太浪費內存空間了。通常的jvm都是8:1。也便是說,Eden區:From區:To區域的比例是:8:1:1。始終有90%的空間是能夠用來建立對象的,而剩下的10%用來存放回收後存活的對象。app

image.png 大概步驟:

  • 1.當Eden區滿的時候,會觸發第一次young gc,把還活着的對象拷貝到Survivor From區;當Eden區再次觸發young gc的時候,會掃描Eden區和From區域,對兩個區域進行垃圾回收,通過此次回收後還存活的對象,則直接複製到To區域,並將Eden和From區域清空。
  • 2.當後續Eden又發生young gc的時候,會對Eden和To區域進行垃圾回收,存活的對象複製到From區域,並將Eden和To區域清空。
  • 3.可見部分對象會在From和To區域中複製來複制去,如此交換15次(由JVM參數MaxTenuringThreshold決定,這個參數默認是15),最終若是仍是存活,就存入到老年代

優勢:在存活對象很少的狀況下,性能高,能解決內存碎片和標記清除算法的引用更新問題

缺點:會形成一部分的內存浪費。不過能夠根據實際狀況,將內存塊大小比例適當調整; 若是存活對象的數量比較大,coping的性能會變得不好。

1.4 分代收集

基於追蹤的垃圾回收算法(標記-清掃、節點複製)一個主要問題是在生命週期較長的對象上浪費時間(長生命週期的對象是不須要頻繁掃描的)。同時,內存分配存在這麼一個事實 「most object die young」。基於這兩點,分代垃圾回收算法將對象按生命週期長短存放到堆上的兩個(或者更多)區域,這些區域就是分代(generation)。對於新生代的區域的垃圾回收頻率要明顯高於老年代區域。

分配對象的時候重新生代裏面分配,若是後面發現對象的生命週期較長,則將其移到老年代,這個過程叫作 promote。隨着不斷 promote,最後新生代的大小在整個堆的佔用比例不會特別大。收集的時候集中主要精力在新生代就會相對來講效率更高,STW 時間也會更短。

優勢:性能更優。生命週期長的對象GC頻率少,生命週期短的對象GC頻率高,大部分對象都是生命週期短的,同時也縮短了STW時間。

缺點:算法實現複雜。

2 go 垃圾回收機制--三色標記法

三色標記法是一種改進的標記清除算法,主要是改進了標記部分,使得垃圾回收與用戶程序併發執行

算法思想:

  • 首先把全部的對象都放到白色的集合中
  • 從根節點開始遍歷對象,遍歷到的白色對象從白色集合中放到灰色集合中
  • 遍歷灰色集合中的對象,把灰色對象引用的白色集合的對象放入到灰色集合中,同時把遍歷過的灰色集合中的對象放到黑色的集合中
  • 循環上一步,直到灰色集合中沒有對象
  • 灰色集合中沒有對象了,白色集合中的對象就是不可達對象,也就是垃圾,進行回收

2.1 寫屏障

Go在進行三色標記的時候並無STW,也就是說,此時的對象仍是能夠進行修改,這個時候會有問題,當在進行三色標記中掃描灰色集合中,掃描到了對象A,並標記了對象A的全部引用,以下圖 image.png 這時候,開始掃描對象D的引用,而此時,另外一個goroutine修改了D->E的引用,變成了以下圖所示 image.png 這樣會不會致使E對象就掃描不到了,而被誤認爲 爲白色對象,也就是垃圾

寫屏障就是爲了解決這樣的問題,引入寫屏障後,在上述步驟後,E會被認爲是存活的,即便後面E被A對象拋棄,E會被在下一輪的GC中進行回收,這一輪GC中是不會對對象E進行回收的。

寫屏障原理:在每一處內存寫操做的前面,編譯器會生成的一小段代碼段,來確保不要打破一些約束條件。即在改變D->E的引用關係到A->E的時候,會有一小段代碼,來禁止這個操做。這一小段代碼就是一個約束條件,這個約束條件就是:黑色對象不能引用白色對象

標記階段:原本標記節點是須要STW,可是爲了避免STW,標記階段與用戶程序也作成了併發,並無STW,標記協程是由多個MarkWorker goroutine 共同完成,它們在回收任務完成前綁定到 P,而後進入休眠狀態,知道被調度器喚醒,他們與用戶協程是併發執行的。

標記階段與用戶程序併發帶來兩個問題:
1.用戶程序新建的對象。對於正在正在進行標記階段,用戶新建的對象可能不會被標記到。所以用戶新建的對象直接認爲是黑色的,本次標記不會標記到它,這樣用戶程序新建對象就沒有問題了。
2.用戶程序修改對象的引用關係。寫屏障不容許黑色的對象直接引用白色的對象,固然寫屏障並非真的不讓黑色對象引用白色對象,而是發⽣⼀個信號,垃圾回收器會捕獲到這樣的信號後就知道這個對象發⽣改變,而後從新掃描這個對象,看看它的引⽤或者被引⽤是否被改變,這樣利⽤狀態的重置從⽽實現當對象狀態發⽣改變的時候依然能夠判斷它是活着的仍是死的。

那標記階段何時會STW呢?

清理階段:不會STW,由於清理的是白色的對象,白色的對象不可能被用戶程序用到,所以清理程序能夠與用戶程序併發執行,不須要STW。

併發清理本質上是一個死循環,被喚醒後開始執行清理任務。 經過遍歷全部span 對象,觸發內存回收器的回收操做。任務完成後,再次休眠,等待下次任務

總結一些GO GC的三色標記過程:

三色標記法方案支持並行,即用戶代碼能夠和GC代碼同時運行。具體來說,Golang GC分爲幾個階段:

  • Mark階段該階段又分爲三個部分:
    • Mark Prepare:初始化GC任務,包括開啓寫屏障(write barrier)和輔助GC(mutator assist),統計root對象的任務數量等,這個過程須要STW
    • GC Drains: 掃描全部root對象,包括全局指針和goroutine(G)棧上的指針(掃描對應G棧時需中止該G),將其加入標記隊列(灰色隊列),並循環處理灰色隊列的對象,直到灰色隊列爲空。該過程後臺並行執行。
    • Mark Termination階段:該階段主要是完成標記工做,從新掃描(re-scan)全局指針和棧。由於GC Drains階段和用戶程序是並行的,因此在GC Drains過程當中可能會有新的對象分配和指針賦值,GC Drains就須要經過寫屏障(write barrier)記錄下來,re-scan 再檢查一下,這個過程也是會STW的。
  • Sweep: 按照標記結果回收全部的白色對象,該過程後臺並行執行。
  • Sweep Termination: 對未清掃的span進行清掃, 只有上一輪的GC的清掃工做完成才能夠開始新一輪的GC。

總結一下,Golang的GC過程有兩次STW:第一次STW會準備根對象的掃描, 啓動寫屏障(Write Barrier)和輔助GC(mutator assist),爲GC Drains階段作準備.第二次STW會從新掃描部分根對象, 禁用寫屏障(Write Barrier)和輔助GC(mutator assist),由於GC Drains階段已結束.

GO GC分紅這麼多階段主要是爲了讓GC與用戶程序併發執行。分紅多個階段,在必須STW的階段纔去STW,這樣其餘階段就能夠跟用戶階段併發執行了。

每一次STW耗時極小,通常在1ms之內。

2.2 GO GC觸發的時間和條件

(1)觸發的時間。在堆上分配大於 32K byte 對象的時候進行檢測此時是否知足垃圾回收條件,若是知足則進行垃圾回收。

(2)觸發的條件。

// gcShouldStart returns true if the exit condition for the _GCoff
// phase has been met. The exit condition should be tested when
// allocating.
//
// If forceTrigger is true, it ignores the current heap size, but
// checks all other conditions. In general this should be false.
func gcShouldStart(forceTrigger bool) bool {
    return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0
}
複製代碼

forceTrigger 是 forceGC 的標誌;後面半句的意思是當前堆上的活躍對象大於咱們初始化時候設置的 GC 觸發閾值。在 malloc 以及 free 的時候 heap_live 會一直進行更新。

3 GO GC調優

1.硬性參數

假設進程所佔內存爲reachable, 則reachable*(1+GOGC/100)=8M 的時候,gc 就會被觸發,開始進行相關的 gc 操做。

所以可根據進程佔用內存大小來調整GOGC參數,減小GC觸發的次數

2.代碼層面的tiops

(1)減小對象分配:所謂減小對象的分配,其實是儘可能作到,對象的重用。 例以下面兩個函數:

func(r*Reader)Read()([]byte,error)
func(r*Reader)Read(buf[]byte)(int,error)
複製代碼

第一個函數沒有入參,所以第一個函數裏面每次都會分配空間並返回;第二個函數有入參buf,所以在函數內部可利用傳入的buf,不須要分配內存。

(2)少作string和[]byte轉化。減小gc壓力

(3)少使用字符串拼接。使用+進行字符串拼接會生成新的對象

(4)提早預知數組大小。數組初始化的時候必定要用make,及時大小是0,不過最好預估一下大小,這樣在append的時候就不會常常去擴容

4 參考

【1】Go 垃圾回收
【2】Golang 垃圾回收剖析
【3】以標記清除的方式垃圾回收
【4】爲何 Go 在 GC 時 STW 的時間很短?
【5】垃圾回收算法(計數、標記、複製),golang垃圾回收
【6】Golang源碼探索(三) GC的實現原理
【7】深刻理解Go-垃圾回收機制
【8】java垃圾回收之複製算法

相關文章
相關標籤/搜索