這一部分主要介紹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 會在某一個時間點上中止本來正在運行的程序,以後它會掃描runtime已經知道的的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使用的入門技巧。
減小對象分配 所謂減小對象的分配,其實是儘可能作到,對象的重用。 好比像以下的兩個函數定義:
1
2 |
func(r*Reader)Read()([]byte,error) func(r*Reader)Read(buf[]byte)(int,error) |
第一個函數沒有形參,每次調用的時候返回一個[]byte,第二個函數在每次調用的時候,形參是一個buf []byte 類型的對象,以後返回讀入的byte的數目。
第一個函數在每次調用的時候都會分配一段空間,這會給gc形成額外的壓力。第二個函數在每次迪調用的時候,會重用形參聲明。
老生常談 string與[]byte轉化 在stirng與[]byte之間進行轉換,會給gc形成壓力 經過gdb,能夠先對比下二者的數據結構:
1
2 3 |
type = struct []uint8 { uint8 *array; int len; int cap;} type = struct string { uint8 *str; int len;} |
二者發生轉換的時候,底層數據結結構會進行復制,所以致使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函數來進行。
可是還有一個弊端,好比參考以下代碼:
1
|
b := make([]int, 1024) b = append(b, 99) fmt.Println("len:", len(b), "cap:", cap(b)) |
在使用了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