图解Go语言内存分配
Go語言內(nèi)置運行時(就是runtime),拋棄了傳統(tǒng)的內(nèi)存分配方式,改為自主管理。這樣可以自主地實現(xiàn)更好的內(nèi)存使用模式,比如內(nèi)存池、預(yù)分配等等。這樣,不會每次內(nèi)存分配都需要進行系統(tǒng)調(diào)用。
Golang運行時的內(nèi)存分配算法主要源自 Google 為 C 語言開發(fā)的 TCMalloc算法,全稱 Thread-CachingMalloc。核心思想就是把內(nèi)存分為多級管理,從而降低鎖的粒度。它將可用的堆內(nèi)存采用二級分配的方式進行管理:每個線程都會自行維護一個獨立的內(nèi)存池,進行內(nèi)存分配時優(yōu)先從該內(nèi)存池中分配,當(dāng)內(nèi)存池不足時才會向全局內(nèi)存池申請,以避免不同線程對全局內(nèi)存池的頻繁競爭。
為了更好的閱讀體驗,手動貼上文章目錄:
基礎(chǔ)概念
Go在程序啟動的時候,會先向操作系統(tǒng)申請一塊內(nèi)存(注意這時還只是一段虛擬的地址空間,并不會真正地分配內(nèi)存),切成小塊后自己進行管理。
申請到的內(nèi)存塊被分配了三個區(qū)域,在X64上分別是512MB,16GB,512GB大小。
arena區(qū)域就是我們所謂的堆區(qū),Go動態(tài)分配的內(nèi)存都是在這個區(qū)域,它把內(nèi)存分割成 8KB大小的頁,一些頁組合起來稱為 mspan。
bitmap區(qū)域標識 arena區(qū)域哪些地址保存了對象,并用 4bit標志位表示對象是否包含指針、 GC標記信息。 bitmap中一個 byte大小的內(nèi)存對應(yīng) arena區(qū)域中4個指針大小(指針大小為 8B )的內(nèi)存,所以 bitmap區(qū)域的大小是 512GB/(4*8B)=16GB。如下圖:
從上圖其實還可以看到bitmap的高地址部分指向arena區(qū)域的低地址部分,也就是說bitmap的地址是由高地址向低地址增長的。
spans區(qū)域存放 mspan(也就是一些 arena分割的頁組合起來的內(nèi)存管理基本單元,后文會再講)的指針,每個指針對應(yīng)一頁,所以 spans區(qū)域的大小就是 512GB/8KB*8B=512MB。除以8KB是計算 arena區(qū)域的頁數(shù),而最后乘以8是計算 spans區(qū)域所有指針的大小。創(chuàng)建 mspan的時候,按頁填充對應(yīng)的 spans區(qū)域,在回收 object時,根據(jù)地址很容易就能找到它所屬的 mspan。
內(nèi)存管理單元
mspan:Go中內(nèi)存管理的基本單元,是由一片連續(xù)的 8KB的頁組成的大塊內(nèi)存。注意,這里的頁和操作系統(tǒng)本身的頁并不是一回事,它一般是操作系統(tǒng)頁大小的幾倍。一句話概括: mspan是一個包含起始地址、 mspan規(guī)格、頁的數(shù)量等內(nèi)容的雙端鏈表。
每個 mspan按照它自身的屬性 SizeClass的大小分割成若干個 object,每個 object可存儲一個對象。并且會使用一個位圖來標記其尚未使用的 object。屬性 SizeClass決定 object大小,而 mspan只會分配給和 object尺寸大小接近的對象,當(dāng)然,對象的大小要小于 object大小。還有一個概念: SpanClass,它和 SizeClass的含義差不多,
Size_Class = Span_Class / 2 = Span_Class / 2這是因為其實每個 SizeClass有兩個 mspan,也就是有兩個 SpanClass。其中一個分配給含有指針的對象,另一個分配給不含有指針的對象。這會給垃圾回收機制帶來利好,之后的文章再談。
如下圖, mspan由一組連續(xù)的頁組成,按照一定大小劃分成 object。
Go1.9.2里 mspan的 SizeClass共有67種,每種 mspan分割的object大小是8*2n的倍數(shù),這個是寫死在代碼里的:
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}const _NumSizeClasses = 67 _NumSizeClasses = 67var 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} 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}根據(jù) mspan的 SizeClass可以得到它劃分的 object大小。 比如 SizeClass等于3, object大小就是32B。 32B大小的object可以存儲對象大小范圍在17B~32B的對象。而對于微小對象(小于16B),分配器會將其進行合并,將幾個對象分配到同一個 object中。
數(shù)組里最大的數(shù)是32768,也就是32KB,超過此大小就是大對象了,它會被特別對待,這個稍后會再介紹。順便提一句,類型 SizeClass為0表示大對象,它實際上直接由堆內(nèi)存分配,而小對象都要通過 mspan來分配。
對于mspan來說,它的 SizeClass會決定它所能分到的頁數(shù),這也是寫死在代碼里的:
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}const _NumSizeClasses = 67 _NumSizeClasses = 67var 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} 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}比如當(dāng)我們要申請一個 object大小為 32B的 mspan的時候,在classtosize里對應(yīng)的索引是3,而索引3在 class_to_allocnpages數(shù)組里對應(yīng)的頁數(shù)就是1。
mspan結(jié)構(gòu)體定義:
}type mspan struct {struct { ? ?//鏈表后向指針,用于將span鏈接起來//鏈表后向指針,用于將span鏈接起來 ? ?next *mspan next *mspan ? ?//鏈表前向指針,用于將span鏈接起來//鏈表前向指針,用于將span鏈接起來 ? ?prev *mspan ? ?// 起始地址,也即所管理頁的地址// 起始地址,也即所管理頁的地址 ? ?startAddr uintptr ? ?// 管理的頁數(shù)// 管理的頁數(shù) ? ?npages uintptr ? ?// 塊個數(shù),表示有多少個塊可供分配// 塊個數(shù),表示有多少個塊可供分配 ? ?nelems uintptr ? ?//分配位圖,每一位代表一個塊是否已分配//分配位圖,每一位代表一個塊是否已分配 ? ?allocBits *gcBits ? ?// 已分配塊的個數(shù)// 已分配塊的個數(shù) ? ?allocCount uint16 ? ?// class表中的class ID,和Size Classs相關(guān)// class表中的class ID,和Size Classs相關(guān) ? ?spanclass spanClass ? ? ?// class表中的對象大小,也即塊大小// class表中的對象大小,也即塊大小 ? ?elemsize uintptr }我們將 mspan放到更大的視角來看:
上圖可以看到有兩個 S指向了同一個 mspan,因為這兩個 S指向的 P是同屬一個 mspan的。所以,通過 arena上的地址可以快速找到指向它的 S,通過 S就能找到 mspan,回憶一下前面我們說的 mspan區(qū)域的每個指針對應(yīng)一頁。
假設(shè)最左邊第一個 mspan的 SizeClass等于10,根據(jù)前面的 class_to_size數(shù)組,得出這個 msapn分割的 object大小是144B,算出可分配的對象個數(shù)是 8KB/144B=56.89個,取整56個,所以會有一些內(nèi)存浪費掉了,Go的源碼里有所有 SizeClass的 mspan浪費的內(nèi)存的大小;再根據(jù) class_to_allocnpages數(shù)組,得到這個 mspan只由1個 page組成;假設(shè)這個 mspan是分配給無指針對象的,那么 spanClass等于20。
startAddr直接指向 arena區(qū)域的某個位置,表示這個 mspan的起始地址, allocBits指向一個位圖,每位代表一個塊是否被分配了對象; allocCount則表示總共已分配的對象個數(shù)。
這樣,左起第一個 mspan的各個字段參數(shù)就如下圖所示:
內(nèi)存管理組件
內(nèi)存分配由內(nèi)存分配器完成。分配器由3種組件構(gòu)成: mcache, mcentral, mheap。
mcache
mcache:每個工作線程都會綁定一個mcache,本地緩存可用的 mspan資源,這樣就可以直接給Goroutine分配,因為不存在多個Goroutine競爭的情況,所以不會消耗鎖資源。
mcache的結(jié)構(gòu)體定義:
numSpanClasses = _NumSizeClasses << 1type mcache struct {struct { ? ?alloc [numSpanClasses]*mspan}numSpanClasses = _NumSizeClasses << 1_NumSizeClasses << 1mcache用 SpanClasses作為索引管理多個用于分配的 mspan,它包含所有規(guī)格的 mspan。它是 _NumSizeClasses的2倍,也就是 67*2=134,為什么有一個兩倍的關(guān)系,前面我們提到過:為了加速之后內(nèi)存回收的速度,數(shù)組里一半的 mspan中分配的對象不包含指針,另一半則包含指針。
對于無指針對象的 mspan在進行垃圾回收的時候無需進一步掃描它是否引用了其他活躍的對象。 后面的垃圾回收文章會再講到,這次先到這里。
mcache在初始化的時候是沒有任何 mspan資源的,在使用過程中會動態(tài)地從 mcentral申請,之后會緩存下來。當(dāng)對象小于等于32KB大小時,使用 mcache的相應(yīng)規(guī)格的 mspan進行分配。
mcentral
mcentral:為所有 mcache提供切分好的 mspan資源。每個 central保存一種特定大小的全局 mspan列表,包括已分配出去的和未分配出去的。 每個 mcentral對應(yīng)一種 mspan,而 mspan的種類導(dǎo)致它分割的 object大小不同。當(dāng)工作線程的 mcache中沒有合適(也就是特定大小的)的 mspan時就會從 mcentral獲取。
mcentral被所有的工作線程共同享有,存在多個Goroutine競爭的情況,因此會消耗鎖資源。結(jié)構(gòu)體定義:
}type mcentral struct {struct { ? ?// 互斥鎖// 互斥鎖 ? ?lock mutex lock mutex ? ?// 規(guī)格// 規(guī)格 ? ?sizeclass int32 ? ?// 尚有空閑object的mspan鏈表// 尚有空閑object的mspan鏈表 ? ?nonempty mSpanList ? ?// 沒有空閑object的mspan鏈表,或者是已被mcache取走的msapn鏈表// 沒有空閑object的mspan鏈表,或者是已被mcache取走的msapn鏈表 ? ?empty mSpanList ? ?// 已累計分配的對象個數(shù)// 已累計分配的對象個數(shù) ? ?nmalloc uint64 }用圖來表示:
empty表示這條鏈表里的 mspan都被分配了 object,或者是已經(jīng)被 cache取走了的 mspan,這個 mspan就被那個工作線程獨占了。而 nonempty則表示有空閑對象的 mspan列表。每個 central結(jié)構(gòu)體都在 mheap中維護。
簡單說下 mcache從 mcentral獲取和歸還 mspan的流程:
獲取 加鎖;從 nonempty鏈表找到一個可用的 mspan;并將其從 nonempty鏈表刪除;將取出的 mspan加入到 empty鏈表;將 mspan返回給工作線程;解鎖。
歸還 加鎖;將 mspan從 empty鏈表刪除;將 mspan加入到 nonempty鏈表;解鎖。
mheap
mheap:代表Go程序持有的所有堆空間,Go程序使用一個 mheap的全局對象 _mheap來管理堆內(nèi)存。
當(dāng) mcentral沒有空閑的 mspan時,會向 mheap申請。而 mheap沒有資源時,會向操作系統(tǒng)申請新內(nèi)存。 mheap主要用于大對象的內(nèi)存分配,以及管理未切割的 mspan,用于給 mcentral切割成小對象。
同時我們也看到, mheap中含有所有規(guī)格的 mcentral,所以,當(dāng)一個 mcache從 mcentral申請 mspan時,只需要在獨立的 mcentral中使用鎖,并不會影響申請其他規(guī)格的 mspan。
mheap結(jié)構(gòu)體定義:
}type mheap struct {struct { ? ?lock mutexlock mutex ? ?// spans: 指向mspans區(qū)域,用于映射mspan和page的關(guān)系// spans: 指向mspans區(qū)域,用于映射mspan和page的關(guān)系 ? ?spans []*mspan ? ?// 指向bitmap首地址,bitmap是從高地址向低地址增長的// 指向bitmap首地址,bitmap是從高地址向低地址增長的 ? ?bitmap uintptr ? ?// 指示arena區(qū)首地址// 指示arena區(qū)首地址 ? ?arena_start uintptr ? ?// 指示arena區(qū)已使用地址位置// 指示arena區(qū)已使用地址位置 ? ?arena_used ?uintptr ? ?// 指示arena區(qū)末地址// 指示arena區(qū)末地址 ? ?arena_end ? uintptr ? ?central [67*2]struct {67*2]struct { ? ? ? ?mcentral mcentral ? ? ? ?pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byteCacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte ? ?}}用圖來表示:
上圖我們看到,bitmap和arena_start指向了同一個地址,這是因為bitmap的地址是從高到低增長的,所以他們指向的內(nèi)存位置相同。
內(nèi)存分配流程
上一篇文章《Golang之變量去哪兒》中我們提到了,變量是在棧上分配還是在堆上分配,是由逃逸分析的結(jié)果決定的。通常情況下,編譯器是傾向于將變量分配到棧上的,因為它的開銷小,最極端的就是"zero garbage",所有的變量都會在棧上分配,這樣就不會存在內(nèi)存碎片,垃圾回收之類的東西。
Go的內(nèi)存分配器在分配對象時,根據(jù)對象的大小,分成三類:小對象(小于等于16B)、一般對象(大于16B,小于等于32KB)、大對象(大于32KB)。
大體上的分配流程:
>32KB 的對象,直接從mheap上分配;
<=16B 的對象使用mcache的tiny分配器分配;
(16B,32KB] 的對象,首先計算對象的規(guī)格大小,然后使用mcache中相應(yīng)規(guī)格大小的mspan分配;
如果mcache沒有相應(yīng)規(guī)格大小的mspan,則向mcentral申請
如果mcentral沒有相應(yīng)規(guī)格大小的mspan,則向mheap申請
如果mheap中也沒有合適大小的mspan,則向操作系統(tǒng)申請
總結(jié)
Go語言的內(nèi)存分配非常復(fù)雜,它的一個原則就是能復(fù)用的一定要復(fù)用。源碼很難追,后面可能會再來一篇關(guān)于內(nèi)存分配的源碼閱讀相關(guān)的文章。簡單總結(jié)一下本文吧。
文章從一個比較粗的角度來看Go的內(nèi)存分配,并沒有深入細節(jié)。一般而言,了解它的原理,到這個程度也可以了。
Go在程序啟動時,會向操作系統(tǒng)申請一大塊內(nèi)存,之后自行管理。
Go內(nèi)存管理的基本單元是mspan,它由若干個頁組成,每種mspan可以分配特定大小的object。
mcache, mcentral, mheap是Go內(nèi)存管理的三大組件,層層遞進。mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供所有線程使用;mheap管理Go的所有動態(tài)分配內(nèi)存。
極小對象會分配在一個object中,以節(jié)省資源,使用tiny分配器分配內(nèi)存;一般小對象通過mspan分配內(nèi)存;大對象則直接由mheap分配內(nèi)存。
更好的閱讀體驗,電腦端打開原文閱讀。
參考資料
【簡單易懂,非常清晰】https://yq.aliyun.com/articles/652551
【內(nèi)存分配器的初始化過程,分配流程圖很詳細】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
【整體感】https://juejin.im/post/59f2e19f5188253d6816d504
【源碼解讀】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
【實際Linux命令】https://mikespook.com/2014/12/%E7%90%86%E8%A7%A3-go-%E8%AF%AD%E8%A8%80%E7%9A%84%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8/
【整體流程圖 對象分配函數(shù)調(diào)用鏈路】http://blog.newbmiao.com/2018/08/20/go-source-analysis-of-memory-alloc.html
【源碼講解 非常細致】https://www.cnblogs.com/zkweb/p/7880099.html
【源碼閱讀】https://zhuanlan.zhihu.com/p/34930748
總結(jié)
以上是生活随笔為你收集整理的图解Go语言内存分配的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Golang之变量去哪儿
- 下一篇: 深入Go的底层,带你走近一群有追求的人