Golang GC 垃圾回收機制詳解

摘要

在實際使用 go 語言的過程當中,碰到了一些看似奇怪的內存佔用現象,因而決定對go語言的垃圾回收模型進行一些研究。本文對研究的結果進行一下總結。javascript

什麼是垃圾回收?

曾幾什麼時候,內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序員必須對內存當心的進行管理操做,控制內存的申請及釋放。稍有不慎,就可能產生內存泄露問題,這種問題不易發現而且難以定位,一直成爲困擾開發者的噩夢。如何解決這個頭疼的問題呢?過去通常採用兩種辦法:php

  • 內存泄露檢測工具。這種工具的原理通常是靜態代碼掃描,經過掃描程序檢測可能出現內存泄露的代碼段。然而檢測工具不免有疏漏和不足,只能起到輔助做用。
  • 智能指針。這是 c++ 中引入的自動內存管理方法,經過擁有自動內存管理功能的指針對象來引用對象,是程序員不用太關注內存的釋放,而達到內存自動釋放的目的。這種方法是採用最普遍的作法,可是對程序員有必定的學習成本(並不是語言層面的原生支持),並且一旦有忘記使用的場景依然沒法避免內存泄露。

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

常見的垃圾回收方法

引用計數(reference counting)

這是最簡單的一種垃圾回收算法,和以前提到的智能指針殊途同歸。對每一個對象維護一個引用計數,當引用該對象的對象被銷燬或更新時被引用對象的引用計數自動減一,當被引用對象被建立或被賦值給其餘對象時引用計數自動加一。當引用計數爲0時則當即回收對象。java

這種方法的優勢是實現簡單,而且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較普遍,如ios cocoa框架,php,python等。簡單引用計數算法也有明顯的缺點:python

  • 頻繁更新引用計數下降了性能。一種簡單的解決方法就是編譯器將相鄰的引用計數更新操做合併到一次更新;還有一種方法是針對頻繁發生的臨時變量引用不進行計數,而是在引用達到0時經過掃描堆棧確認是否還有臨時對象引用而決定是否釋放。等等還有不少其餘方法,具體能夠參考這裏。
  • 循環引用問題。當對象間發生循環引用時引用鏈中的對象都沒法獲得釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指針和weak指針兩種指針類型。或者系統檢測循環引用並主動打破循環鏈。固然這也增長了垃圾回收的複雜度。

標記-清除(mark and sweep)

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

分代收集(generation)

通過大量實際觀察得知,在面向對象編程語言中,絕大多數對象的生命週期都很是短。分代收集的基本思想是,將堆劃分爲兩個或多個稱爲 代(generation)的空間。新建立的對象存放在稱爲 新生代(young generation)中(通常來講,新生代的大小會比 老年代小不少),隨着垃圾回收的重複執行,生命週期較長的對象會被 提高(promotion)到老年代中。所以,新生代垃圾回收和老年代垃圾回收兩種不一樣的垃圾回收方式應運而生,分別用於對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度很是快,比老年代快幾個數量級,即便新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是由於大多數對象的生命週期都很短,根本無需提高到老年代。c++

GO的垃圾回收器

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

  • 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等各類方法,依然沒有發現緣由。最後發現原來這時正常的…主要的緣由有兩個,github

一是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的壓力,提高代碼的效率。

參考資料

https://talks.golang.org/2015/go-gc.pdf

https://www.zhihu.com/question/21615032

https://blog.golang.org/go15gc

golang gc 中文入門(總結比較全面 包括golang gc 在不一樣版本的比較 贊) http://www.open-open.com/lib/view/open1435846881544.html(原文)

其餘垃圾回收相關文章

這個介紹的 gc 較爲系統: http://newhtml.net/v8-garbage-collection/

1.5 版本的垃圾回收器 http://ruizeng.net/go-15-release-notes/

內存泄露參考 http://blog.csdn.net/na_he/article/details/7429171

Go1.5 源碼剖析 https://github.com/qyuhen/book

手動管理golang gc的一個例子(比較深層次的內容) http://my.oschina.net/lubia/blog/175154

相關文章
相關標籤/搜索