GO GC 垃圾回收機制

垃圾回收(Garbage Collection,簡稱GC)是編程語言中提供的內存管理功能。javascript

在傳統的系統級編程語言(主要指C/C++)中,程序員定義了一個變量,就是在內存中開闢了一段相應的空間來存值。因爲內存是有限的,因此當程序再也不須要使用某個變量的時候,就須要銷燬該對象並釋放其所佔用的內存資源,好從新利用這段空間。在C/C++中,釋放無用變量內存空間的事情須要由程序員本身來處理。就是說當程序員認爲變量沒用了,就手動地釋放其佔用的內存。可是這樣顯然很是繁瑣,若是有所遺漏,就可能形成資源浪費甚至內存泄露。當軟件系統比較複雜,變量多的時候程序員每每就忘記釋放內存或者在不應釋放的時候釋放內存了。這對於程序開發人員是一個比較頭痛的問題。php

爲了解決這個問題,後來開發出來的幾乎全部新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而沒必要關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對再也不使用的內存資源進行自動回收的功能就被稱爲垃圾回收。java

垃圾回收常見的方法

引用計數(reference counting)python

引用計數經過在對象上增長本身被引用的次數,被其餘對象引用時加1,引用本身的對象被回收時減1,引用數爲0的對象即爲能夠被回收的對象。這種算法在內存比較緊張和實時性比較高的系統中使用的比較普遍,如ios cocoa框架,php,python等。ios

優勢:git

一、方式簡單,回收速度快。程序員

缺點:github

一、須要額外的空間存放計數。golang

二、沒法處理循環引用(如a.b=b;b.a=a這種狀況)。算法

三、頻繁更新引用計數下降了性能。

標記-清除(mark and sweep)

該方法分爲兩步,標記從根變量開始迭代得遍歷全部被引用的對象,對可以經過應用遍歷訪問到的對象都進行標記爲「被引用」;標記完成後進行清除操做,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操做)。這種方法解決了引用計數的不足,可是也有比較明顯的問題:每次啓動垃圾回收都會暫停當前全部的正常代碼執行,回收是系統響應能力大大下降!固然後續也出現了不少mark&sweep算法的變種(如三色標記法)優化了這個問題。

複製收集

複製收集的方式只須要對對象進行一次掃描。準備一個「新的空間」,從根開始,對對象進行掃,若是存在對這個對象的引用,就把它複製到「新空間中」。一次掃描結束以後,全部存在於「新空間」的對象就是全部的非垃圾對象。

這兩種方式各有千秋,標記清除的方式節省內存可是兩次掃描須要更多的時間,對於垃圾比例較小的狀況佔優點。複製收集更快速可是須要額外開闢一塊用來複制的內存,對垃圾比例較大的狀況佔優點。特別的,複製收集有「局部性」的優勢。

在複製收集的過程當中,會按照對象被引用的順序將對象複製到新空間中。因而,關係較近的對象被放在距離較近的內存空間的可能性會提升,這叫作局部性。局部性高的狀況下,內存緩存會更有效地運做,程序的性能會提升。

對於標記清除,有一種標記-壓縮算法的衍生算法:

對於壓縮階段,它的工做就是移動全部的可達對象到堆內存的同一個區域中,使他們緊湊的排列在一塊兒,從而將全部非可達對象釋放出來的空閒內存都集中在一塊兒,經過這樣的方式來達到減小內存碎片的目的。

分代收集(generation)

這種收集方式用了程序的一種特性:大部分對象會從產生開始在很短的時間內變成垃圾,而存在的很長時間的對象每每都有較長的生命週期。

根據對象的存活週期不一樣將內存劃分爲新生代和老年代,存活週期短的爲新生代,存活週期長的爲老年代。這樣就能夠根據每塊內存的特色採用最適當的收集算法。

新建立的對象存放在稱爲 新生代(young generation)中(通常來講,新生代的大小會比 老年代小不少)。高頻對新生成的對象進行回收,稱爲「小回收」,低頻對全部對象回收,稱爲「大回收」。每一次「小回收」事後,就把存活下來的對象歸爲老年代,「小回收」的時候,遇到老年代直接跳過。大多數分代回收算法都採用的「複製收集」方法,由於小回收中垃圾的比例較大。

這種方式存在一個問題:若是在某個新生代的對象中,存在「老生代」的對象對它的引用,它就不是垃圾了,那麼怎麼制止「小回收」對其回收呢?這裏用到了一中叫作寫屏障的方式。

程序對全部涉及修改對象內容的地方進行保護,被稱爲「寫屏障」(Write Barrier)。寫屏障不只用於分代收集,也用於其餘GC算法中。

在此算法的表現是,用一個記錄集來記錄重新生代到老生代的引用。若是有兩個對象A和B,當對A的對象內容進行修改並加入B的引用時,若是①A是「老生代」②B是「新生代」。則將這個引用加入到記錄集中。「小回收」的時候,由於記錄集中有對B的引用,因此B再也不是垃圾。

三色標記算法

三色標記算法是對標記階段的改進,原理以下:

  1. 起初全部對象都是白色。
  2. 從根出發掃描全部可達對象,標記爲灰色,放入待處理隊列。
  3. 從隊列取出灰色對象,將其引用對象標記爲灰色放入隊列,自身標記爲黑色。
  4. 重複 3,直到灰色對象隊列爲空。此時白色對象即爲垃圾,進行回收。

可視化以下。

三色標記的一個明顯好處是可以讓用戶程序和 mark 併發的進行,具體能夠參考論文:《On-the-fly garbage collection: an exercise in cooperation.》。Golang 的 GC 實現也是基於這篇論文,後面再具體說明。

GO的垃圾回收器

go語言垃圾回收整體採用的是經典的mark and sweep算法。

  • v1.3之前版本 STW(Stop The World)

    golang的垃圾回收算法都很是簡陋,而後其性能也廣被詬病:go runtime在必定條件下(內存超過閾值或按期如2min),暫停全部任務的執行,進行mark&sweep操做,操做完成後啓動全部任務的執行。在內存使用較多的場景下,go程序在進行垃圾回收時會發生很是明顯的卡頓現象(Stop The World)。在對響應速度要求較高的後臺服務進程中,這種延遲簡直是不能忍受的!這個時期國內外不少在生產環境實踐go語言的團隊都或多或少踩過gc的坑。當時解決這個問題比較經常使用的方法是儘快控制自動分配內存的內存數量以減小gc負荷,同時採用手動管理內存的方法處理須要大量及高頻分配內存的場景。

  • v1.3 Mark STW, Sweep 並行

    1.3版本中,go runtime分離了mark和sweep操做,和之前同樣,也是先暫停全部任務執行並啓動mark,mark完成後立刻就從新啓動被暫停的任務了,而是讓sweep任務和普通協程任務同樣並行的和其餘任務一塊兒執行。若是運行在多核處理器上,go會試圖將gc任務放到單獨的核心上運行而儘可能不影響業務代碼的執行。go team本身的說法是減小了50%-70%的暫停時間。

  • v1.5 三色標記法

    go 1.5正在實現的垃圾回收器是「非分代的、非移動的、併發的、三色的標記清除垃圾收集器」。引入了上文介紹的三色標記法,這種方法的mark操做是能夠漸進執行的而不需每次都掃描整個內存空間,能夠減小stop the world的時間。 由此能夠看到,一路走來直到1.5版本,go的垃圾回收性能也是一直在提高,可是相對成熟的垃圾回收系統(如java jvm和javascript v8),go須要優化的路徑還很長(可是相信將來必定是美好的~)。

  • v1.8 混合寫屏障(hybrid write barrier)

    這個版本的 GC 代碼相比以前改動仍是挺大的,採用一種混合的 write barrier 方式 (Yuasa-style deletion write barrier [Yuasa ‘90] 和 Dijkstra-style insertion write barrier [Dijkstra ‘78])來避免 堆棧從新掃描。

    混合屏障的優點在於它容許堆棧掃描永久地使堆棧變黑(沒有STW而且沒有寫入堆棧的障礙),這徹底消除了堆棧從新掃描的須要,從而消除了對堆棧屏障的需求。從新掃描列表。特別是堆棧障礙在整個運行時引入了顯着的複雜性,而且干擾了來自外部工具(如GDB和基於內核的分析器)的堆棧遍歷。

    此外,與Dijkstra風格的寫屏障同樣,混合屏障不須要讀屏障,所以指針讀取是常規的內存讀取; 它確保了進步,由於物體單調地從白色到灰色再到黑色。

    混合屏障的缺點很小。它可能會致使更多的浮動垃圾,由於它會在標記階段的任什麼時候刻保留從根(堆棧除外)可到達的全部內容。然而,在實踐中,當前的Dijkstra障礙可能幾乎保留不變。混合屏障還禁止某些優化:特別是,若是Go編譯器能夠靜態地顯示指針是nil,則Go編譯器當前省略寫屏障,可是在這種狀況下混合屏障須要寫屏障。這可能會略微增長二進制大小。

小結:

經過go team多年對gc的不斷改進和憂化,GC的卡頓問題在1.8 版本基本上能夠作到 1 毫秒如下的 GC 級別。 實際上,gc低延遲是有代價的,其中最大的是吞吐量的降低。因爲須要實現並行處理,線程間同步和多餘的數據生成複製都會佔用實際邏輯業務代碼運行的時間。GHC的全局中止GC對於實現高吞吐量來講是十分合適的,而Go則更擅長與低延遲。
並行GC的第二個代價是不可預測的堆空間擴大。程序在GC的運行期間仍能不斷分配任意大小的堆空間,所以咱們須要在到達最大的堆空間以前實行一次GC,可是過早實行GC會形成沒必要要的GC掃描,這也是須要衡量利弊的。所以在使用Go時,須要自行保證程序有足夠的內存空間。

垃圾收集是一個難題,沒有所謂十全十美的方案,一般是爲了適應應用場景作出的一種取捨。

相信GO將來會更好。

參考:

https://github.com/golang/pro...

http://legendtkl.com/2017/04/...

https://blog.twitch.tv/gos-ma...

https://blog.plan99.net/moder...

links

相關文章
相關標籤/搜索