以下由golang教學專欄跟大家介紹Golang GC 垃圾回收機制的詳解,希望對需要的朋友有幫助!
實際使用go在語言的過程中,碰到了一些看似奇怪的記憶體佔用現象,於是決定對go語言的垃圾回收模型做一些研究。本文將研究的結果進行一下總結。
曾幾何時,記憶體管理是程式設計師開發應用的一大難題。傳統的系統層級程式語言(主要指C/C )中,程式設計師必須對記憶體小心的進行管理操作,控制記憶體的申請及釋放。稍有不慎,就可能產生內存洩漏問題,這種問題不易發現且難以定位,一直成為困擾開發者的惡夢。如何解決這個頭痛的問題呢?過去一般採用兩種辦法:
為了解決這個問題,後來開發出來的幾乎所有新語言(java,python,php等等)都引入了語言層面的自動記憶體管理– 也就是語言的使用者只用關注記憶體的申請而不必關心記憶體的釋放,記憶體釋放由虛擬機器(virtual machine)或執行時(runtime)來自動進行管理。而這種對不再使用的記憶體資源進行自動回收的行為就被稱為垃圾回收。
這是最簡單的一種垃圾回收演算法,和先前提到的智慧指標異曲同工。對每個物件維護一個引用計數,當引用該物件的物件被銷毀或更新時被引用物件的引用計數自動減一,當被引用物件被建立或被賦值給其他物件時引用計數自動加一。當引用計數為0時則立即回收物件。
這種方法的優點是實現簡單,並且記憶體的回收很及時。這種演算法在記憶體比較緊張和即時性比較高的系統中使用的比較廣泛,如ios cocoa框架,php,python等。簡單引用計數演算法也有明顯的缺點:
該方法分為兩步,標記從根變數開始迭代得遍歷所有被引用的對象,對能夠透過應用遍歷訪問到的物件都被標記為「被引用」;標記完成後進行清除操作,對沒有標記過的記憶體進行回收(回收同時可能伴隨碎片整理操作)。這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停目前所有的正常程式碼執行,回收是系統回應能力大幅降低!當然後續也出現了許多mark&sweep演算法的變種(如三色標記法)優化了這個問題。
經過大量實際觀察得知,在物件導向程式語言中,絕大多數物件的生命週期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為 代(generation)的空間。新創建的物件存放在稱為新生代(young generation)中(一般來說,新生代的大小會比老年代小得多),隨著垃圾回收的重複執行,生命週期較長的物件會被提升( promotion)到老年代中。因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用於對各自空間中的物件執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命週期都很短,根本無需提升到老年代。
go語言垃圾回收總體採用的是經典的mark and sweep演算法。
團隊在實踐go語言時同樣碰到最多和最棘手的問題也是記憶體問題(其中以gc為主),這裡把遇到的問題和經驗總結下,歡迎大家一起交流探討。
這個問題在我們對後台服務進行壓力測試時發現,我們模擬大量的用戶請求訪問後台服務,這時各服務模組能觀察到明顯的記憶體佔用上升。但是當停止壓測時,記憶體佔用並未發生明顯的下降。花了很長時間定位問題,使用gprof等各種方法,依然沒有發現原因。最後發現原來這時正常的…主要的原因有兩個,
一是go的垃圾回收有個觸發閾值,這個閾值會隨著每次記憶體使用變大而逐漸增大(如初始閾值是10MB則下一次就是20MB,再下一次就成為了40MB…) ,如果長時間沒有觸發gc go會主動觸發一次(2min)。高峰時內存使用量上去後,除非持續申請內存,靠閾值觸發gc基本上已經不可能,而是要等最多2min主動gc開始才能觸發gc。
第二個原因是go語言在向系統交還記憶體時只是告訴系統這些記憶體不需要使用了,可以回收;同時作業系統會採取「拖延症」策略,並不是立即回收,而是等到系統記憶體緊張時才會開始回收這樣該程式又重新申請記憶體時就可以獲得極快的分配速度。
對於對使用者回應事件有要求的後端程序,golang gc時的stop the world兼職是惡夢。根據上文的介紹,1.5版本的go再完成上述改進後應該gc性能會提升不少,但是所有的垃圾回收型語言都難免在gc時面臨性能下降,對此我們對於應該盡量避免頻繁創建臨時堆物件(如&abc{}, new, make等)以減少垃圾收集時的掃描時間,對於需要頻繁使用的臨時物件考慮直接透過陣列快取進行重用;很多人採用cgo的方法自己管理記憶體而繞過垃圾收集,這種方法除非迫不得已個人是不推薦的(容易造成不可預測的問題),當然迫不得已的情況下還是可以考慮的,這招帶來的效果還是很明顯的~
我們的一個服務需要處理很多長連接請求,實現時,對於每個長連接請求各開了一個讀取和寫入協程,全部採用endless for loop不停地處理收發數據。當連線被遠端關閉後,如果不對這兩個協程做處理,他們依然會一直運行,並且佔用的channel也不會被釋放…這裡就必須十分注意,在不使用協程後一定要把他依賴的channel close並透過再協程中判斷channel是否關閉以保證其退出。
APR 30TH, 2016 8:02 PM | COMMENTS
這一部分主要介紹golang gc的一些入門的相關知識,由於gc內容涉及比較多,一點一點慢慢整理。
主要參的這個:
http://morsmachine.dk/machine-gc
是14年寫的,估計那時候的gc機制還比較simple,新版的golang對gc的改動應該會比較大
還有那個go語言讀書筆記中關於golang gc 的相關部分
「記憶體外洩」(Memory Leak)這個詞看似自己很熟悉,但實際上卻也從來沒有看過它的準確意義。
記憶體洩漏,是從作業系統的角度上來闡述的,形象的比喻就是「作業系統可提供給所有進程的儲存空間(虛擬記憶體空間)正在被某個進程榨乾”,導致的原因是程式在運行的時候,會不斷地動態開闢的存儲空間,這些存儲空間在運行結束之後後並沒有被及時釋放掉。應用程式在分配了某段記憶體之後,由於設計的錯誤,會導致程式失去了對該段記憶體的控制,造成了記憶體空間的浪費。
如果程式在內存空間內申請了一塊內存,之後程式運行結束之後,沒有把這塊內存空間釋放掉,而且對應的程序又沒有很好的gc機制去對程序申請的空間進行回收,這樣就會導致記憶體外洩。
從使用者的角度來說,記憶體外洩本身不會有什麼危害,因為這不是對使用者功能的影響,但是「記憶體外洩」如果進
對C 和C 這種沒有Garbage Collection 的語言來講,我們主要關注兩種類型的記憶體洩漏:
記憶體外洩涉及到的相關問題還有很多,這裡暫不展開討論。
具體的優缺點可以參考這個,這裡只是進行大致介紹。
因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生(先分類,之後再對症下藥),分別用於對各自空間中的物件執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命週期都很短,根本無需提升到老年代。
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 的引進可以說就是為了解決記憶體回收的問題。新開發的語言(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 的之星效率,判斷它到底是否對程式的運作造成了影響呢?第一種方式是設定godebug 的環境變量,具體可以參考這一篇,真的是講的很好的文章:鏈接,比如運行GODEBUG=gctrace=1 ./myserver
,如果要想對於輸出結果了解,還需要對於gc 的原理進行更進一步的深入分析,這篇文章的好處在於,清晰的之處了golang 的gc 時間是由哪些因素決定的,因此也可以針對性的採取不同的方式提升gc 的時間:
根據先前的分析也可以知道,golang 中的gc 是使用標記清楚法,所以gc 的總時間為:
Tgc = Tseq Tmark Tsweep
( T 表示time)
之後粒度進一步細分,具體的概念還是有些不太懂:
涉及演算法的問題,總是會有些參數。 GOGC 參數主要控制的是下一次 gc 開始的時候的記憶體使用量。
例如目前的程式使用了4M 的對記憶體(這裡說的是堆記憶體),即是說程式目前reachable 的記憶體為4m,當程式佔用的記憶體達到reachable* (1 GOGC/100)=8M 的時候,gc 就會被觸發,開始進行相關的gc 操作。
如何對 GOGC 的參數進行設置,要根據生產情況中的實際場景來定,例如 GOGC 參數提升,來減少 GC 的頻率。
想要有深入的 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中文網其他相關文章!