[toc]html
Go語言內置運行時(就是runtime),拋棄了傳統的內存分配方式,改成自主管理。這樣能夠自主地實現更好的內存使用模式,好比內存池、預分配等等。這樣,不會每次內存分配都須要進行系統調用。linux
Golang運行時的內存分配算法主要源自 Google 爲 C 語言開發的TCMalloc算法
,全稱Thread-Caching Malloc
。核心思想就是把內存分爲多級管理,從而下降鎖的粒度。它將可用的堆內存採用二級分配的方式進行管理:每一個線程都會自行維護一個獨立的內存池,進行內存分配時優先從該內存池中分配,當內存池不足時纔會向全局內存池申請,以免不一樣線程對全局內存池的頻繁競爭。git
Go在程序啓動的時候,會先向操做系統申請一塊內存(注意這時還只是一段虛擬的地址空間,並不會真正地分配內存),切成小塊後本身進行管理。github
申請到的內存塊被分配了三個區域,在X64上分別是512MB,16GB,512GB大小。golang
arena區域
就是咱們所謂的堆區,Go動態分配的內存都是在這個區域,它把內存分割成8KB
大小的頁,一些頁組合起來稱爲mspan
。web
bitmap區域
標識arena
區域哪些地址保存了對象,而且用4bit
標誌位表示對象是否包含指針、GC
標記信息。bitmap
中一個byte
大小的內存對應arena
區域中4個指針大小(指針大小爲 8B )的內存,因此bitmap
區域的大小是512GB/(4*8B)=16GB
。算法
從上圖其實還能夠看到bitmap的高地址部分指向arena區域的低地址部分,也就是說bitmap的地址是由高地址向低地址增加的。數組
spans區域
存放mspan
(也就是一些arena
分割的頁組合起來的內存管理基本單元,後文會再講)的指針,每一個指針對應一頁,因此spans
區域的大小就是512GB/8KB*8B=512MB
。除以8KB是計算arena
區域的頁數,而最後乘以8是計算spans
區域全部指針的大小。建立mspan
的時候,按頁填充對應的spans
區域,在回收object
時,根據地址很容易就能找到它所屬的mspan
。緩存
mspan
:Go中內存管理的基本單元,是由一片連續的8KB
的頁組成的大塊內存。注意,這裏的頁和操做系統自己的頁並非一回事,它通常是操做系統頁大小的幾倍。一句話歸納:mspan
是一個包含起始地址、mspan
規格、頁的數量等內容的雙端鏈表。ide
每一個mspan
按照它自身的屬性Size Class
的大小分割成若干個object
,每一個object
可存儲一個對象。而且會使用一個位圖來標記其還沒有使用的object
。屬性Size Class
決定object
大小,而mspan
只會分配給和object
尺寸大小接近的對象,固然,對象的大小要小於object
大小。還有一個概念:Span Class
,它和Size Class
的含義差很少,
Size_Class = Span_Class / 2
這是由於其實每一個 Size Class
有兩個mspan
,也就是有兩個Span Class
。其中一個分配給含有指針的對象,另外一個分配給不含有指針的對象。這會給垃圾回收機制帶來利好,以後的文章再談。
以下圖,mspan
由一組連續的頁組成,按照必定大小劃分紅object
。
Go1.9.2裏mspan
的Size Class
共有67種,每種mspan
分割的object大小是8*2n的倍數,這個是寫死在代碼裏的:
// path: /usr/local/go/src/runtime/sizeclasses.go const _NumSizeClasses = 67 var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
根據mspan
的Size Class
能夠獲得它劃分的object
大小。 好比Size Class
等於3,object
大小就是32B。 32B大小的object能夠存儲對象大小範圍在17B~32B的對象。而對於微小對象(小於16B),分配器會將其進行合併,將幾個對象分配到同一個object
中。
數組裏最大的數是32768,也就是32KB,超過此大小就是大對象了,它會被特別對待,這個稍後會再介紹。順便提一句,類型Size Class
爲0表示大對象,它實際上直接由堆內存分配,而小對象都要經過mspan
來分配。
對於mspan來講,它的Size Class
會決定它所能分到的頁數,這也是寫死在代碼裏的:
// path: /usr/local/go/src/runtime/sizeclasses.go const _NumSizeClasses = 67 var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
好比當咱們要申請一個object
大小爲32B
的mspan
的時候,在class_to_size裏對應的索引是3,而索引3在class_to_allocnpages
數組裏對應的頁數就是1。
mspan
結構體定義:
// path: /usr/local/go/src/runtime/mheap.go type mspan struct { //鏈表前向指針,用於將span連接起來 next *mspan //鏈表前向指針,用於將span連接起來 prev *mspan // 起始地址,也即所管理頁的地址 startAddr uintptr // 管理的頁數 npages uintptr // 塊個數,表示有多少個塊可供分配 nelems uintptr //分配位圖,每一位表明一個塊是否已分配 allocBits *gcBits // 已分配塊的個數 allocCount uint16 // class表中的class ID,和Size Classs相關 spanclass spanClass // class表中的對象大小,也即塊大小 elemsize uintptr }
咱們將mspan
放到更大的視角來看:
上圖能夠看到有兩個S
指向了同一個mspan
,由於這兩個S
指向的P
是同屬一個mspan
的。因此,經過arena
上的地址能夠快速找到指向它的S
,經過S
就能找到mspan
,回憶一下前面咱們說的mspan
區域的每一個指針對應一頁。
假設最左邊第一個mspan
的Size Class
等於10,根據前面的class_to_size
數組,得出這個msapn
分割的object
大小是144B,算出可分配的對象個數是8KB/144B=56.89
個,取整56個,因此會有一些內存浪費掉了,Go的源碼裏有全部Size Class
的mspan
浪費的內存的大小;再根據class_to_allocnpages
數組,獲得這個mspan
只由1個page
組成;假設這個mspan
是分配給無指針對象的,那麼spanClass
等於20。
startAddr
直接指向arena
區域的某個位置,表示這個mspan
的起始地址,allocBits
指向一個位圖,每位表明一個塊是否被分配了對象;allocCount
則表示總共已分配的對象個數。
這樣,左起第一個mspan
的各個字段參數就以下圖所示:
內存分配由內存分配器完成。分配器由3種組件構成:mcache
, mcentral
, mheap
。
mcache
:每一個工做線程都會綁定一個mcache,本地緩存可用的mspan
資源,這樣就能夠直接給Goroutine分配,由於不存在多個Goroutine競爭的狀況,因此不會消耗鎖資源。
mcache
的結構體定義:
//path: /usr/local/go/src/runtime/mcache.go type mcache struct { alloc [numSpanClasses]*mspan } numSpanClasses = _NumSizeClasses << 1
mcache
用Span Classes
做爲索引管理多個用於分配的mspan
,它包含全部規格的mspan
。它是_NumSizeClasses
的2倍,也就是67*2=134
,爲何有一個兩倍的關係,前面咱們提到過:爲了加速以後內存回收的速度,數組裏一半的mspan
中分配的對象不包含指針,另外一半則包含指針。
對於無指針對象的mspan
在進行垃圾回收的時候無需進一步掃描它是否引用了其餘活躍的對象。 後面的垃圾回收文章會再講到,此次先到這裏。
mcache
在初始化的時候是沒有任何mspan
資源的,在使用過程當中會動態地從mcentral
申請,以後會緩存下來。當對象小於等於32KB大小時,使用mcache
的相應規格的mspan
進行分配。
mcentral
:爲全部mcache
提供切分好的mspan
資源。每一個central
保存一種特定大小的全局mspan
列表,包括已分配出去的和未分配出去的。 每一個mcentral
對應一種mspan
,而mspan
的種類致使它分割的object
大小不一樣。當工做線程的mcache
中沒有合適(也就是特定大小的)的mspan
時就會從mcentral
獲取。
mcentral
被全部的工做線程共同享有,存在多個Goroutine競爭的狀況,所以會消耗鎖資源。結構體定義:
//path: /usr/local/go/src/runtime/mcentral.go type mcentral struct { // 互斥鎖 lock mutex // 規格 sizeclass int32 // 尚有空閒object的mspan鏈表 nonempty mSpanList // 沒有空閒object的mspan鏈表,或者是已被mcache取走的msapn鏈表 empty mSpanList // 已累計分配的對象個數 nmalloc uint64 }
empty
表示這條鏈表裏的mspan
都被分配了object
,或者是已經被cache
取走了的mspan
,這個mspan
就被那個工做線程獨佔了。而nonempty
則表示有空閒對象的mspan
列表。每一個central
結構體都在mheap
中維護。
簡單說下mcache
從mcentral
獲取和歸還mspan
的流程:
獲取 加鎖;從nonempty
鏈表找到一個可用的mspan
;並將其從nonempty
鏈表刪除;將取出的mspan
加入到empty
鏈表;將mspan
返回給工做線程;解鎖。
歸還 加鎖;將mspan
從empty
鏈表刪除;將mspan
加入到nonempty
鏈表;解鎖。
mheap
:表明Go程序持有的全部堆空間,Go程序使用一個mheap
的全局對象_mheap
來管理堆內存。
當mcentral
沒有空閒的mspan
時,會向mheap
申請。而mheap
沒有資源時,會向操做系統申請新內存。mheap
主要用於大對象的內存分配,以及管理未切割的mspan
,用於給mcentral
切割成小對象。
同時咱們也看到,mheap
中含有全部規格的mcentral
,因此,當一個mcache
從mcentral
申請mspan
時,只須要在獨立的mcentral
中使用鎖,並不會影響申請其餘規格的mspan
。
mheap
結構體定義:
//path: /usr/local/go/src/runtime/mheap.go type mheap struct { lock mutex // spans: 指向mspans區域,用於映射mspan和page的關係 spans []*mspan // 指向bitmap首地址,bitmap是從高地址向低地址增加的 bitmap uintptr // 指示arena區首地址 arena_start uintptr // 指示arena區已使用地址位置 arena_used uintptr // 指示arena區末地址 arena_end uintptr central [67*2]struct { mcentral mcentral pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte } }
上圖咱們看到,bitmap和arena_start指向了同一個地址,這是由於bitmap的地址是從高到低增加的,因此他們指向的內存位置相同。
上一篇文章《Golang之變量去哪兒》中咱們提到了,變量是在棧上分配仍是在堆上分配,是由逃逸分析的結果決定的。一般狀況下,編譯器是傾向於將變量分配到棧上的,由於它的開銷小,最極端的就是"zero garbage",全部的變量都會在棧上分配,這樣就不會存在內存碎片,垃圾回收之類的東西。
Go的內存分配器在分配對象時,根據對象的大小,分紅三類:小對象(小於等於16B)、通常對象(大於16B,小於等於32KB)、大對象(大於32KB)。
大致上的分配流程:
32KB 的對象,直接從mheap上分配;
Go語言的內存分配很是複雜,它的一個原則就是能複用的必定要複用。源碼很難追,後面可能會再來一篇關於內存分配的源碼閱讀相關的文章。簡單總結一下本文吧。
文章從一個比較粗的角度來看Go的內存分配,並無深刻細節。通常而言,瞭解它的原理,到這個程度也能夠了。
【簡單易懂,很是清晰】https://yq.aliyun.com/articles/652551
【內存分配器的初始化過程,分配流程圖很詳細】https://www.jianshu.com/p/47691d870756
【全局的圖】https://swanspouse.github.io/2018/08/22/golang-memory-model/
【雨痕 Go1.5源碼閱讀】https://github.com/qyuhen/book
【圖不錯】https://www.jianshu.com/p/47691d870756
【總體感】http://www.javashuo.com/article/p-fchuskqa-gh.html
【源碼解讀】http://legendtkl.com/2017/04/02/golang-alloc/
【重點推薦 深刻到晶體管了 圖很好】https://www.linuxzen.com/go-memory-allocator-visual-guide.html
【整體描述對象分配流程】http://gocode.cc/project/4/article/103
【總體流程圖 對象分配函數調用鏈路】http://blog.newbmiao.com/2018/08/20/go-source-analysis-of-memory-alloc.html
【源碼講解 很是細緻】http://www.javashuo.com/article/p-ncezupmk-ke.html