一.Golang內存管理
我們先來對比下C與Golang的內存分配:
Golang內存分配特點:
- 預先從操作系統申請一大塊內存。
- 內存分配算法採用Google的 TCMalloc算法,預先將申請的內存分成不同大小的內存集合,給不同場景的內存使用。
- 回收內存會放入內存池,並不會直接分配給操作系統。
介紹TCMalloc的幾個重要概念
- Page:操作系統對內存管理以頁爲單位,大小爲8KB。
- Span:一組連續的Page被稱爲Span,代碼中是mspan,mspan是TCMalloc中內存管理的基本單位。
- mcache:每個p各自的Cache,一個Cache包含多個空閒內存塊鏈表,每個鏈表連接的都是內存塊,同一個鏈表上內存塊的大小是相同的,也可以說按內存塊大小,給內存塊分了個類,這樣可以根據申請的內存大小,快速從合適的鏈表選擇空閒內存塊。由於每個協程有自己的ThreadCache,所以ThreadCache訪問是無鎖的。
- mcentral:mcentral是所有協程共享的緩存,也是保存的空閒內存塊鏈表需加鎖訪問。
- mheap:mheap是對堆內存的抽象,mheap存了兩棵樹,鏈表保存的是Span。當CentralCache的內存不足時,會從PageHeap獲取空閒的內存Span,然後把1個Span拆成若干內存塊,添加到對應大小的鏈表中並分配內存;當CentralCache的內存過多時,會把空閒的內存塊放回mheap中。
二.內存逃逸
內存逃逸的概念:
Go語言裏沒有一個關鍵字或者函數可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。變量被存儲在堆上的過程叫做逃逸。
常見的逃逸類型:
- 申請過大的空間:make的大小設定的很大。
- 引用指針變量一定逃逸(返回值、參數,直接修改指針變量),一定會逃逸。
- Slice、Map、Channel裏面有指針,一定會逃逸。
- 調用接口類型的方法,一定發生逃逸。
所以,我們經常討論的一個問題,結構體是值傳遞還是指針傳遞?值傳遞會有內存拷貝,分配在棧區。指針傳遞會逃逸在棧區,雖然沒有內存拷貝的過程,但GC壓力會變大。如果使用很長的slice或map,還是用指針傳遞吧。
三.Golang的GC機制
- 首先創建三個集合:白、灰、黑。
- 將所有對象放入白色集合中。
- 然後從根節點開始遍歷所有對象(注意這裏並不遞歸遍歷),把遍歷到的對象從白色集合放入灰色集合。
- 之後遍歷灰色集合,將灰色對象引用的對象從白色集合放入灰色集合,之後將此灰色對象放入黑色集合。
- 重複 4 直到灰色中無任何對象。
- 通過write-barrier檢測對象有變化,重複以上操作。
- 收集所有白色對象(垃圾)。