帶你領略Go原始碼的魅力----Go記憶體原理詳解
1、記憶體分割槽
程式碼經過預處理、編譯、彙編、連結4步後生成一個可執行程式。
在 Windows 下,程式是一個普通的可執行檔案,以下列出一個二進位制可執行檔案的基本情況:
通過上圖可以得知,在沒有執行程式前,也就是說程式沒有載入到記憶體前,可執行程式內部已經分好三段資訊,分別為程式碼區(text)、**資料區(data)和未初始化資料區(bss)**3 個部分。
有些人直接把data和bss合起來叫做靜態區或全域性區。
1、1 程式碼區(text)
存放 CPU 執行的機器指令。通常程式碼區是可共享的(即另外的執行程式可以呼叫它),使其可共享的目的是對於頻繁被執行的程式,只需要在記憶體中有一份程式碼即可。程式碼區通常是只讀
的,使其只讀的原因是防止程式意外地修改了它的指令。另外,程式碼區還規劃了局部變數的相關資訊。
1、2 全域性初始化資料區/靜態資料區(data)
該區包含了在程式中明確被初始化的全域性變數、已經初始化的靜態變數(包括全域性靜態變數和區域性靜態變數)和常量資料(如字串常量)。
1、3 未初始化資料區(bss)
存入的是全域性未初始化變數和未初始化靜態變數。未初始化資料區的資料在程式開始執行之前被核心初始化為 0 或者空(nil)。
程式在載入到記憶體前,程式碼區和全域性區(data和bss)的大小就是固定的,程式執行期間不能改變。
然後,執行可執行程式,系統把程式載入到記憶體,除了根據可執行程式的資訊分出程式碼區(text)、資料區(data)和未初始化資料區(bss)之外,還額外增加了棧區
、堆區。
1、4 棧區(stack)
棧是一種先進後出的記憶體結構,由編譯器自動分配釋放,存放函式的引數值、返回值、區域性變數等。
在程式執行過程中實時載入和釋放,因此,區域性變數的生存週期為申請到釋放該段棧空間。
1、5 堆區(heap)
堆是一個大容器,它的容量要遠遠大於棧,但沒有棧那樣先進後出的順序。用於動態記憶體分配。堆在記憶體中位於BSS區和棧區之間。
根據語言的不同,如C語言、C++語言,一般由程式設計師分配和釋放,若程式設計師不釋放,程式結束時由作業系統回收。
Go語言、Java、python等都有垃圾回收機制(GC),用來自動釋放記憶體。
2、 Go Runtime記憶體分配
Go語言內建執行時(就是Runtime),拋棄了傳統的記憶體分配方式,改為自主管理。這樣可以自主地實現更好的記憶體使用模式,比如記憶體池、預分配等等。這樣,不會每次記憶體分配都需要進行系統呼叫。
Golang執行時的記憶體分配演演算法主要源自 Google 為 C 語言開發的TCMalloc演演算法,全稱Thread-Caching Malloc。
核心思想就是把記憶體分為多級管理,從而降低鎖的粒度。它將可用的堆記憶體採用二級分配的方式進行管理。
每個執行緒都會自行維護一個獨立的記憶體池,進行記憶體分配時優先從該記憶體池中分配,當記憶體池不足時才會向全域性記憶體池申請,以避免不同執行緒對全域性記憶體池的頻繁競爭。
2、1 基本策略
- 每次從作業系統申請一大塊記憶體,以減少系統呼叫。
- 將申請的大塊記憶體按照特定的大小預先的進行切分成小塊,構成連結串列。
- 為物件分配記憶體時,只需從大小合適的連結串列提取一個小塊即可。
- 回收物件記憶體時,將該小塊記憶體重新歸還到原連結串列,以便複用。
- 如果閒置記憶體過多,則嘗試歸還部分記憶體給作業系統,降低整體開銷。
**注意:**記憶體分配器只管理記憶體塊,並不關心物件狀態,而且不會主動回收,垃圾回收機制在完成清理操作後,觸發記憶體分配器的回收操作
2、2 記憶體管理單元
分配器將其管理的記憶體塊分為兩種:
- span:由多個連續的頁(page [大小:8KB])組成的大塊記憶體。
- object:將span按照特定大小切分成多個小塊,每一個小塊都可以儲存物件。
用途:
span 面向內部管理
object 面向物件分配
//path:Go SDK/src/runtime/malloc.go _PageShift = 13 _PageSize = 1 << _PageShift //8KB 複製程式碼
在基本策略中講到,Go在程式啟動的時候,會先向作業系統申請一塊記憶體,切成小塊後自己進行管理。
申請到的記憶體塊被分配了三個區域,在X64上分別是512MB,16GB,512GB大小。
**注意:**這時還只是一段虛擬的地址空間,並不會真正地分配記憶體
arena區域
就是所謂的堆區,Go動態分配的記憶體都是在這個區域,它把記憶體分割成8KB大小的頁,一些頁組合起來稱為mspan。
//path:Go SDK/src/runtime/mheap.go type mspan struct { next *mspan // 雙向連結串列中 指向下一個 prev *mspan // 雙向連結串列中 指向前一個 startAddr uintptr // 起始序號 npages uintptr // 管理的頁數 manualFreeList gclinkptr // 待分配的 object 連結串列 nelems uintptr // 塊個數,表示有多少個塊可供分配 allocCount uint16 // 已分配塊的個數 ... } 複製程式碼
bitmap區域
標識arena區域哪些地址儲存了物件,並且用4bit標誌位表示物件是否包含指標、GC標記資訊。
spans區域
存放mspan的指標,每個指標對應一頁,所以spans區域的大小就是512GB/8KB*8B=512MB。
除以8KB是計算arena區域的頁數,而最後乘以8是計算spans區域所有指標的大小。
2、3 記憶體管理元件
記憶體分配由記憶體分配器完成。分配器由3種元件構成:
cache
每個執行期工作執行緒都會繫結一個cache,用於無鎖 object 的分配
central
為所有cache提供切分好的後備span資源
heap
管理閒置span,需要時向作業系統申請記憶體
2、3、1 cache
cache:每個工作執行緒都會繫結一個mcache,本地快取可用的mspan資源。
這樣就可以直接給Go Routine分配,因為不存在多個Go Routine競爭的情況,所以不會消耗鎖資源。
mcache 的結構體定義:
//path:Go SDK/src/runtime/mcache.go _NumSizeClasses = 67 //67 numSpanClasses = _NumSizeClasses << 1 //134 type mcache struct { alloc [numSpanClasses]*mspan //以numSpanClasses 為索引管理多個用於分配的 span } 複製程式碼
mcache用Span Classes作為索引管理多個用於分配的mspan,它包含所有規格的mspan。
它是 _NumSizeClasses 的2倍,也就是67*2=134,為什麼有一個兩倍的關係。
為了加速之後記憶體回收的速度,陣列裡一半的mspan中分配的物件不包含指標,另一半則包含指標。對於無指標物件的mspan在進行垃圾回收的時候無需進一步掃描它是否引用了其他活躍的物件。
2、3、2 central
central:為所有mcache提供切分好的mspan資源。
每個central儲存一種特定大小的全域性mspan列表,包括已分配出去的和未分配出去的。
每個mcentral對應一種mspan,而mspan的種類導致它分割的object大小不同。
//path:Go SDK/src/runtime/mcentral.go type mcentral struct { lock mutex // 互斥鎖 sizeclass int32 // 規格 nonempty mSpanList // 尚有空閒object的mspan連結串列 empty mSpanList // 沒有空閒object的mspan連結串列,或者是已被mcache取走的msapn連結串列 nmalloc uint64 // 已累計分配的物件個數 } 複製程式碼
2、3、3 heap
heap:代表Go程式持有的所有堆空間,Go程式使用一個mheap的全域性物件_mheap來管理堆記憶體。
當mcentral沒有空閒的mspan時,會向mheap申請。而mheap沒有資源時,會向作業系統申請新記憶體。mheap主要用於大物件的記憶體分配,以及管理未切割的mspan,用於給mcentral切割成小物件。
同時我們也看到,mheap中含有所有規格的mcentral,所以,當一個mcache從mcentral申請mspan時,只需要在獨立的mcentral中使用鎖,並不會影響申請其他規格的mspan。
//path:Go SDK/src/runtime/mheap.go type mheap struct { lock mutex spans []*mspan // spans: 指向mspans區域,用於對映mspan和page的關係 bitmap uintptr // 指向bitmap首地址,bitmap是從高地址向低地址增長的 arena_start uintptr // 指示arena區首地址 arena_used uintptr // 指示arena區已使用地址位置 arena_end uintptr // 指示arena區末地址 central [numSpanClasses]struct { mcentral mcentral pad [sys.CacheLineSize-unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte } //每個 central 對應一種 sizeclass } 複製程式碼
2、4 分配流程
- 計算待分配物件的規格(size_class)
- 從cache.alloc陣列中找到規格相同的span
- 從span.manualFreeList連結串列提取可用object
- 如果span.manualFreeList為空,從central獲取新的span
- 如果central.nonempty為空,從heap.free/freelarge獲取,並切分成object連結串列
- 如果heap沒有大小合適的span,向作業系統申請新的記憶體
2、5 釋放流程
- 將標記為可回收的object交還給所屬的span.freelist
- 該span被放回central,可以提供cache重新獲取
- 如果span以全部回收object,將其交還給heap,以便重新分切複用
- 定期掃描heap裡閒置的span,釋放其佔用的記憶體
注意:以上流程不包含大物件,它直接從heap分配和釋放
2、6 總結
Go語言的記憶體分配非常複雜,它的一個原則就是能複用的一定要複用。
- Go在程式啟動時,會向作業系統申請一大塊記憶體,之後自行管理。
- Go記憶體管理的基本單元是mspan,它由若干個頁組成,每種mspan可以分配特定大小的object。
- mcache,mcentral,mheap是Go記憶體管理的三大元件,層層遞進。mcache管理執行緒在本地快取的mspan;mcentral管理全域性的mspan供所有執行緒使用;mheap管理Go的所有動態分配記憶體。
- 一般小物件通過mspan分配記憶體;大物件則直接由mheap分配記憶體。
接下來是Go語言曾經的一大黑點:垃圾回收(GC)。可以關注我們的公開課,法師會帶著大家一起深入瞭解Go語言的GC發展和機制,掃一掃二維碼,觀看公開課的直播。