首頁 > 後端開發 > Golang > 關於Golang GC 垃圾回收機制的詳解

關於Golang GC 垃圾回收機制的詳解

藏色散人
發布: 2020-09-14 09:47:08
轉載
3415 人瀏覽過

以下由golang教學專欄跟大家介紹Golang GC 垃圾回收機制的詳解,希望對需要的朋友有幫助!

關於Golang GC 垃圾回收機制的詳解

#摘要

實際使用go在語言的過程中,碰到了一些看似奇怪的記憶體佔用現象,於是決定對go語言的垃圾回收模型做一些研究。本文將研究的結果進行一下總結。

什麼是垃圾回收?

曾幾何時,記憶體管理是程式設計師開發應用的一大難題。傳統的系統層級程式語言(主要指C/C )中,程式設計師必須對記憶體小心的進行管理操作,控制記憶體的申請及釋放。稍有不慎,就可能產生內存洩漏問題,這種問題不易發現且難以定位,一直成為困擾開發者的惡夢。如何解決這個頭痛的問題呢?過去一般採用兩種辦法:

  • 記憶體外洩偵測工具。這種工具的原理一般是靜態程式碼掃描,透過掃描程式偵測可能出現記憶體外洩的程式碼片段。然而檢測工具難免有疏漏和不足,只能起到輔助作用。
  • 智慧指標。這是 c 中引入的自動記憶體管理方法,透過擁有自動記憶體管理功能的指針對象來引用對象,是程式設計師不用太關注記憶體的釋放,而達到記憶體自動釋放的目的。這種方法是採用最廣泛的做法,但是對程式設計師有一定的學習成本(並非語言層面的原生支援),而且一旦有忘記使用的場景依然無法避免記憶體外洩。

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

常見的垃圾回收方法

引用計數(reference counting)

這是最簡單的一種垃圾回收演算法,和先前提到的智慧指標異曲同工。對每個物件維護一個引用計數,當引用該物件的物件被銷毀或更新時被引用物件的引用計數自動減一,當被引用物件被建立或被賦值給其他物件時引用計數自動加一。當引用計數為0時則立即回收物件。

這種方法的優點是實現簡單,並且記憶體的回收很及時。這種演算法在記憶體比較緊張和即時性比較高的系統中使用的比較廣泛,如ios cocoa框架,php,python等。簡單引用計數演算法也有明顯的缺點:

  • 頻繁更新引用計數降低了效能。一個簡單的解決方法是編譯器將相鄰的引用計數更新操作合併到一次更新;還有一種方法是針對頻繁發生的臨時變數引用不進行計數,而是在引用達到0時透過掃描堆疊確認是否還有臨時物件引用而決定是否釋放。等等還有很多其他方法,可以參考這裡。
  • 循環引用問題。當物件間發生循環引用時引用鏈中的物件都無法得到釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指標和weak指標兩種指標類型。或者係統偵測循環引用並主動打破循環鏈。當然這也增加了垃圾回收的複雜度。

標記-清除(mark and sweep)

該方法分為兩步,標記從根變數開始迭代得遍歷所有被引用的對象,對能夠透過應用遍歷訪問到的物件都被標記為「被引用」;標記完成後進行清除操作,對沒有標記過的記憶體進行回收(回收同時可能伴隨碎片整理操作)。這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停目前所有的正常程式碼執行,回收是系統回應能力大幅降低!當然後續也出現了許多mark&sweep演算法的變種(如三色標記法)優化了這個問題。

分代收集(generation)

經過大量實際觀察得知,在物件導向程式語言中,絕大多數物件的生命週期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為 代(generation)的空間。新創建的物件存放在稱為新生代(young generation)中(一般來說,新生代的大小會比老年代小得多),隨著垃圾回收的重複執行,生命週期較長的物件會被提升( promotion)到老年代中。因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用於對各自空間中的物件執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命週期都很短,根本無需提升到老年代。

GO的垃圾回收器

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

  • 1.3版本以前,golang的垃圾回收演算法都非常簡陋,然後其性能也廣被詬病:go runtime在一定條件下(內存超過閾值或定期如2min),暫停所有任務的執行,進行mark&sweep操作,操作完成後啟動所有任務的執行。在記憶體使用較多的場景下,go程式在進行垃圾回收時會發生非常明顯的卡頓現象(Stop The World)。在對反應速度要求較高的後台服務進程中,這種延遲簡直是不能忍受的!這個時期國內外很多在生產環境實踐go語言的團隊都或多或少踩過gc的坑。當時解決這個問題比較常用的方法是盡快控制自動分配記憶體的記憶體數量以減少gc負荷,同時採用手動管理記憶體的方法處理需要大量及高頻分配記憶體的場景。
  • 1.3版本開始go team開始對gc性能進行持續的改進和優化,每個新版的go發佈時gc改進都成為大家備受關注的要點。 1.3版本中,go runtime分離了mark和sweep操作,和之前一樣,也是先暫停所有任務執行並啟動mark,mark完成後馬上就重新啟動被暫停的任務了,而是讓sweep任務和普通協程任務一樣並行的和其他任務一起執行。如果運行在多核心處理器上,go會試圖將gc任務放到單獨的核心上運行而盡量不影響業務程式碼的執行。 go team自己的說法是減少了50%-70%的暫停時間。
  • 1.4版本(目前最新穩定版)對gc的效能改變並不多。 1.4版本中runtime很多程式碼取代了原生c語言實作而採用了go語言實現,對gc帶來的一大改變是可以是實現精確的gc。 c語言實現在gc時無法獲取到內存的對象信息,因此無法準確區分普通變量和指針,只能將普通變量當做指針,如果碰巧這個普通變量指向的空間有其他對象,那這個對象就不會被回收。而go語言實作是完全知道對象的型別訊息,在標記時只會遍歷指標指向的對象,這樣就避免了C實作時的堆記憶體浪費(解決約10-30%)。
  • 1.5版本go team對gc又進行了比較大的改進(1.4中已經埋下伏筆如write barrier的引入),官方的主要目標是減少延遲。 go 1.5正在實現的垃圾回收器是「非分代的、非移動的、並發的、三色的標記清除垃圾收集器」。分代演算法上文已經提及,是一種比較好的垃圾回收管理策略,然1.5版本中並未考慮實現;我猜測的原因是步子不能邁太大,得逐步改進,go官方也表示會在1.6版本的gc優化中考慮。同時引入了上文介紹的三色標記法,這種方法的mark操作是可以漸進執行的而不需每次都掃描整個內存空間,可以減少stop the world的時間。由此可以看到,一路走來直到1.5版本,go的垃圾回收性能也是一直在提升,但是相對成熟的垃圾回收系統(如java jvm和javascript v8),go需要優化的路徑還很長(但相信未來一定是美好的~)。

實踐經驗

團隊在實踐go語言時同樣碰到最多和最棘手的問題也是記憶體問題(其中以gc為主),這裡把遇到的問題和經驗總結下,歡迎大家一起交流探討。

go程式記憶體佔用大的問題

這個問題在我們對後台服務進行壓力測試時發現,我們模擬大量的用戶請求訪問後台服務,這時各服務模組能觀察到明顯的記憶體佔用上升。但是當停止壓測時,記憶體佔用並未發生明顯的下降。花了很長時間定位問題,使用gprof等各種方法,依然沒有發現原因。最後發現原來這時正常的…主要的原因有兩個,

一是go的垃圾回收有個觸發閾值,這個閾值會隨著每次記憶體使用變大而逐漸增大(如初始閾值是10MB則下一次就是20MB,再下一次就成為了40MB…) ,如果長時間沒有觸發gc go會主動觸發一次(2min)。高峰時內存使用量上去後,除非持續申請內存,靠閾值觸發gc基本上已經不可能,而是要等最多2min主動gc開始才能觸發gc。

第二個原因是go語言在向系統交還記​​憶體時只是告訴系統這些記憶體不需要使用了,可以回收;同時作業系統會採取「拖延症」策略,並不是立即回收,而是等到系統記憶體緊張時才會開始回收這樣該程式又重新申請記憶體時就可以獲得極快的分配速度。

gc時間長的問題

對於對使用者回應事件有要求的後端程序,golang gc時的stop the world兼職是惡夢。根據上文的介紹,1.5版本的go再完成上述改進後應該gc性能會提升不少,但是所有的垃圾回收型語言都難免在gc時面臨性能下降,對此我們對於應該盡量避免頻繁創建臨時堆物件(如&abc{}, new, make等)以減少垃圾收集時的掃描時間,對於需要頻繁使用的臨時物件考慮直接透過陣列快取進行重用;很多人採用cgo的方法自己管理記憶體而繞過垃圾收集,這種方法除非迫不得已個人是不推薦的(容易造成不可預測的問題),當然迫不得已的情況下還是可以考慮的,這招帶來的效果還是很明顯的~

goroutine洩露的問題

我們的一個服務需要處理很多長連接請求,實現時,對於每個長連接請求各開了一個讀取和寫入協程,全部採用endless for loop不停地處理收發數據。當連線被遠端關閉後,如果不對這兩個協程做處理,他們依然會一直運行,並且佔用的channel也不會被釋放…這裡就必須十分注意,在不使用協程後一定要把他依賴的channel close並透過再協程中判斷channel是否關閉以保證其退出。

Golang-gc基本知識

APR 30TH, 2016 8:02 PM | COMMENTS

這一部分主要介紹golang gc的一些入門的相關知識,由於gc內容涉及比較多,一點一點慢慢整理。

Golang GC的背景

  • golang是基於garbage collection的語言,這是它的設計原則。
  • 作為一個有垃圾回收器的語言,gc與程式互動時候的效率會影響到整個程式的運作效率。
  • 通常程式本身的記憶體管理會影響gc和程式之間的效率,甚至造成效能瓶頸。

Golang GC的相關問題

主要參的這個:

http://morsmachine.dk/machine-gc

是14年寫的,估計那時候的gc機制還比較simple,新版的golang對gc的改動應該會比較大

還有那個go語言讀書筆記中關於golang gc 的相關部分

#關於記憶體洩漏

「記憶體外洩」(Memory Leak)這個詞看似自己很熟悉,但實際上卻也從來沒有看過它的準確意義。

記憶體洩漏,是從作業系統的角度上來闡述的,形象的比喻就是「作業系統可提供給所有進程的儲存空間(虛擬記憶體空間)正在被某個進程榨乾”,導致的原因是程式在運行的時候,會不斷地動態開闢的存儲空間,這些存儲空間在運行結束之後後並沒有被及時釋放掉。應用程式在分配了某段記憶體之後,由於設計的錯誤,會導致程式失去了對該段記憶體的控制,造成了記憶體空間的浪費。

如果程式在內存空間內申請了一塊內存,之後程式運行結束之後,沒有把這塊內存空間釋放掉,而且對應的程序又沒有很好的gc機制去對程序申請的空間進行回收,這樣就會導致記憶體外洩。

從使用者的角度來說,記憶體外洩本身不會有什麼危害,因為這不是對使用者功能的影響,但是「記憶體外洩」如果進

對C 和C 這種沒有Garbage Collection 的語言來講,我們主要關注兩種類型的記憶體洩漏:

  • 堆記憶體洩漏(Heap leak)。對內存指的是程式運行中根據需要分配透過 malloc,realloc new 等從堆中分配的一塊內存,再是完成後必須透過呼叫對應的 free 或者 delete 刪除。如果程式的設計的錯誤導致這部分記憶體沒有被釋放,那麼此後這塊記憶體將不會被使用,就會產生Heap Leak.
  • 系統資源外洩(Resource Leak)。主要指程式使用系統分配的資源如Bitmap,handle ,SOCKET 等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能降低,系統運作不穩定。

記憶體外洩涉及到的相關問題還有很多,這裡暫不展開討論。

常見的 GC 模式

具體的優缺點可以參考這個,這裡只是進行大致介紹。

  • 引用計數(reference counting)每個物件維護一個引用計數器,當引用該物件的物件被銷毀或更新的時候,被引用物件的引用計數器會自動減1,當被應用的物件被創建,或賦值給其他物件時,引用1,引用為0 的時候回收,思路簡單,但是頻繁更新引用計數器降低效能,存在循環以引用(php,Python所使用的)
  • #標記清除(mark and sweep)就是golang 所使用的,從根變量來時遍歷所有被引用對象,標記之後進行清除操作,對未標記對象進行回收,缺點:每次垃圾回收的時候都會暫停所有的正常運作的程式碼,系統的反應能力會大幅降低,各種mark&swamp 變種(三色標記法),緩解效能問題。
  • 分代蒐集(generation)jvm 就所使用的分代回收的想法。在物件導向程式語言中,絕大多數物件的生命週期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為代(generation)的空間。新創建的物件存放在稱為新生代(young generation)中(一般來說,新生代的大小會比老年代小得多),隨著垃圾回收的重複執行,生命週期較長的對象會被提升(promotion)到老年代中(這裡用到了一個分類的思路,這個是也是科學思考的一個基本思路)。

因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生(先分類,之後再對症下藥),分別用於對各自空間中的物件執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命週期都很短,根本無需提升到老年代。

golang 中的gc 通常是如何工作的

golang 中的gc 基本上是標記清除的思路:

在記憶體堆中(由於有的時候管理內存頁的時候要用到堆的資料結構,所以稱為堆內存)儲存著有一系列的對象,這些對象可能會與其他對像有關聯(references between these objects) a tracing garbage collector 會在某一個時間點上停止原本正在執行的程序,之後它會掃描runtim e已經知道的的object 集合(already known set of objects),通常它們是存在於stack 中的全域變數以及各種物件。 gc 會對這些物件進行標記,將這些物件的狀態標記為可達,從中找出所有的,從目前的這些物件可以達到其他地方的物件的reference,並且將這些物件也標記為可達的對象,這個步驟被稱為mark phase,即標記階段,這一步的主要目的是用來取得這些物件的狀態資訊。

一旦將所有的這些物件都掃描完,gc 就會取得到所有的無法reach 的物件(狀態為unreachable 的物件),並且將它們回收,這一步稱為sweep phase,即是清掃階段

gc 僅僅蒐集那些未被標記為可達(reachable)的物件。如果 gc 沒有辨識出一個 reference,最後有可能會將一個仍在使用的物件給回收掉,就造成了程式運行錯誤。

可以看到主要的三個步驟:掃描,回收,清掃。

感覺比起其他的語言,golang 中的垃圾回收模型還是相對簡單的。

gc中的問題

gc 的引進可以說就是為了解決記憶體回收的問題。新開發的語言(java,python,php等等),在使用的時候,可以使用戶不必關心內存對象的釋放,只需要關心對象的申請即可,通過在runtime 或者在vm 中進行相關的操作,達到自動管理記憶體空間的效果,這種對不再使用的記憶體資源進行自動回收的行為就被稱為垃圾回收。

根據前面的表述,能否正常識別一個 reference 是 gc 能夠正常工作的基礎,因此第一個問題就是 gc 應該如何識別一個 reference?

最大的問題:對於 reference 的辨識比較難,machine code 很難知道,怎麼算是一個reference。如果錯漏掉了一個 reference,就會使得,原本沒有準備好要被 free 掉的內存現在被錯誤地 free 掉,所以策略就是寧多勿少。

一種策略是把所有的 memory 空間都看做是有可能的 references(指標值)。這種稱為保守型垃圾回收器(conservative garbage collector)。 C 中的 Boehm garbage collector 就是這樣運作的。就是說把記憶體中的普通變數也當做指標一樣去處理,盡量 cover 到所有的指標的情況,如果碰巧這個普通的變數值所指向的空間有其他的對象,那麼這個物件是不會被回收的。而 go 語言實作是完全知道對象的型別訊息,在標記時只會遍歷指標指向的對象,這樣就避免了 C 實作時的堆記憶體浪費(解決約 10-30% )。

三色標記

2014/6 1.3 引入並發清理(垃圾回收和使用者邏輯並發執行?)

2015/8 1.5 引入三色標記法

關於並發清理的引入,參考的是這裡在1.3 版本中,go runtime 分離了mark 和sweep 的操作,和以前一樣,也是先暫停所有任務執行並啟動mark( mark 這部分還是要把原程序停下來的),mark 完成後就馬上就重新啟動被暫停的任務了,並且讓sweep 任務和普通協程任務一樣並行,和其他任務一起執行。如果運行在多核心處理器上,go 會試圖將 gc 任務放到單獨的核心上運行而盡量不影響業務代碼的執行,go team 自己的說法是減少了 50%-70% 的暫停時間。

基本演算法就是先前提到的清掃 回收,Golang gc 優化的核心就是盡量使得 STW(Stop The World) 的時間越來越短。

如何測量 GC

之前說了那麼多,那如何測量 gc 的之星效率,判斷它到底是否對程式的運作造成了影響呢?第一種方式是設定godebug 的環境變量,具體可以參考這一篇,真的是講的很好的文章:鏈接,比如運行GODEBUG=gctrace=1 ./myserver,如果要想對於輸出結果了解,還需要對於gc 的原理進行更進一步的深入分析,這篇文章的好處在於,清晰的之處了golang 的gc 時間是由哪些因素決定的,因此也可以針對性的採取不同的方式提升gc 的時間:

根據先前的分析也可以知道,golang 中的gc 是使用標記清楚法,所以gc 的總時間為:

Tgc = Tseq Tmark Tsweep( T 表示time)

  • Tseq 表示是停止使用者的goroutine 和做一些準備活動(通常很小)需要的時間
  • Tmark 是堆標記時間,標記發生在所有使用者goroutine 停止時,因此可以顯著地影響處理的延遲
  • Tsweep 是堆清除時間,清除通常與正常的程式運行同時發生,所以對延遲來說是不太關鍵的

之後粒度進一步細分,具體的概念還是有些不太懂:

  • 與Tmark 相關的:1 垃圾回收過程中,堆中活動物件的數量, 2 帶有指標的活動物件所佔據的記憶體總量3 活動物件中的指標數量。
  • 與Tsweep 相關的:1 堆內存的總量2 堆中的垃圾總量

如何進行gc 調優( gopher 大會Danny )

#硬性參數

涉及演算法的問題,總是會有些參數。 GOGC 參數主要控制的是下一次 gc 開始的時候的記憶體使用量

例如目前的程式使用了4M 的對記憶體(這裡說的是堆記憶體),即是說程式目前reachable 的記憶體為4m,當程式佔用的記憶體達到reachable* (1 GOGC/100)=8M 的時候,gc 就會被觸發,開始進行相關的gc 操作。

如何對 GOGC 的參數進行設置,要根據生產情況中的實際場景來定,例如 GOGC 參數提升,來減少 GC 的頻率。

小tips

想要有深入的 insights,使用 gdb 時必不可少的了,這篇文章裡面整理了一些 gdb 使用的入門技巧。

減少物件分配 所謂減少物件的分配,其實是盡量做到,物件的重複使用。例如像如下的兩個函數定義:

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

第一個函數在每次呼叫的時候都會分配一段空間,這會對 gc 造成額外的壓力。第二個函數在每次迪呼叫的時候,會重複使用形參宣告。

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

兩者發生轉換的時候,底層資料結結構會進行複製,因此導致 gc 效率會變低。解決策略上,一種方式是一直使用 []byte,特別是在資料傳輸方面,[]byte 中也包含著許多 string 會常用到的有效的操作。另一種是使用更為底層的操作直接進行轉化,避免複製行為的發生。可以參考微信「雨痕學堂」中效能優化的第一部分,主要是使用 unsafe.Pointer 直接進行轉換。

對於unsafe 的使用,感覺可以單獨整理一出一篇文章來了,先把相關資料列在這裡http://studygolang.com/articles/685 直觀上,可以把unsafe.Pointer 理解成c 中的void*,在golang 中,相當於是各種類型的指標轉換的橋樑。

關於 uintptr 的底層型別是 int,它可以裝下指標所指的位址的值。它可以和 unsafe.Pointer 進行相互轉化,主要的區別是,uintptr 可以參與指標運算,而 unsafe.Pointer 只能進行指標轉化,不能進行指標運算。想要用 golang 做指標運算,可以參考這個。具體指標運算的時候,要先轉換成 uintptr 的型別,才能進一步計算,例如偏移多少之類的。

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

但是還有一個弊端,例如參考如下程式碼:

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

以上是關於Golang GC 垃圾回收機制的詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:csdn.net
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板