在實際使用go語言的過程當中,碰到了一些看似奇怪的內存佔用現象,因而決定對go語言的垃圾回收模型進行一些研究。本文對研究的結果進行一下總結。javascript
曾幾什麼時候,內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序員必須對內存當心的進行管理操做,控制內存的申請及釋放。稍有不慎,就可能產生內存泄露問題,這種問題不易發現而且難以定位,一直成爲困擾開發者的噩夢。如何解決這個頭疼的問題呢?過去通常採用兩種辦法:php
爲了解決這個問題,後來開發出來的幾乎全部新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而沒必要關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對再也不使用的內存資源進行自動回收的行爲就被稱爲垃圾回收。java
這是最簡單的一種垃圾回收算法,和以前提到的智能指針殊途同歸。對每一個對象維護一個引用計數,當引用該對象的對象被銷燬或更新時被引用對象的引用計數自動減一,當被引用對象被建立或被賦值給其餘對象時引用計數自動加一。當引用計數爲0時則當即回收對象。python
這種方法的優勢是實現簡單,而且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較普遍,如ios cocoa框架,php,python等。簡單引用計數算法也有明顯的缺點:ios
該方法分爲兩步,標記從根變量開始迭代得遍歷全部被引用的對象,對可以經過應用遍歷訪問到的對象都進行標記爲「被引用」;標記完成後進行清除操做,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操做)。這種方法解決了引用計數的不足,可是也有比較明顯的問題:每次啓動垃圾回收都會暫停當前全部的正常代碼執行,回收是系統響應能力大大下降!固然後續也出現了不少mark&sweep算法的變種(如三色標記法)優化了這個問題。c++
通過大量實際觀察得知,在面向對象編程語言中,絕大多數對象的生命週期都很是短。分代收集的基本思想是,將堆劃分爲兩個或多個稱爲 代(generation)的空間。新建立的對象存放在稱爲 新生代(young generation)中(通常來講,新生代的大小會比 老年代小不少),隨着垃圾回收的重複執行,生命週期較長的對象會被 提高(promotion)到老年代中。所以,新生代垃圾回收和老年代垃圾回收兩種不一樣的垃圾回收方式應運而生,分別用於對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度很是快,比老年代快幾個數量級,即便新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是由於大多數對象的生命週期都很短,根本無需提高到老年代。程序員
go語言垃圾回收整體採用的是經典的mark and sweep算法。golang
團隊在實踐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是否關閉以保證其退出。