Golang內存管理之內存分配器詳解
0. 簡介
程序中的數據都會被分配到程序所在的虛擬內存中,內存空間包含兩個重要區(qū)域:棧(Stack) 和 堆(Heap)。函數調用的參數、返回值和局部變量大部分會分配在棧上,這部分由編譯器管理。堆內存的管理方式視語言而定:
- C/C++等編程語言的堆內存由工程師主動申請和釋放;
- Go、Java等編程語言由工程師和編譯器/運行時共同管理,其內存由內存分配器分配,由垃圾回收器回收。
本文就介紹一下Go語言的內存分配器。
1. Go內存分配設計原理
Go內存分配器的設計思想來源于TCMalloc,全稱是Thread-Caching Malloc,核心思想是把內存分為多級管理,利用緩存的思想提升內存使用效率,降低鎖的粒度。
在堆內存管理上分為三個內存級別:
- 線程緩存(MCache):作為線程獨立的內存池,與線程的第一交互內存,訪問無需加鎖;
- 中心緩存(MCentral):作為線程緩存的下一級,是多個線程共享的,所以訪問時需要加鎖;
- 頁堆(MHeap):中心緩存的下一級,在遇到32KB以上的對象時,會直接選擇頁堆分配大內存,而當頁堆內存不夠時,則會通過系統調用向系統申請內存。
1.1 內存管理基本單元mspan
//go:notinheap
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
list *mSpanList // For debugging. TODO: Remove.
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
freeindex uintptr
allocBits *gcBits
gcmarkBits *gcBits
allocCache uint64
...
}runtime.mspan是Go內存管理的基本單元,其結構體中包含的next和prev指針,分別指向前后的runtime.mspan,所以其串聯后的結構是一個雙向鏈表。
而startAddr表示此mspan的起始地址,npages表示管理的頁數,每頁大小8KB,這個頁不是操作系統的內存頁,一般是操作系統內存頁的整數倍。
其它字段:
freeindex— 掃描頁中空閑對象的初始索引;allocBits和gcmarkBits— 分別用于標記內存的占用和回收情況;allocCache—allocBits的補碼,可以用于快速查找內存中未被使用的內存;
注意使用//go:notinheap標記次結構體mspan為非堆上類型,保證此類型對象不會逃逸到堆上。
圖示:

跨度類
在mspan中有一個字段是spanclass,稱為跨度類,是對mspan大小級別的劃分,每個mspan能夠存放指定范圍大小的對象,32KB以內的小對象在Go中,會對應不同大小的內存刻度Size Class,Size Class和Object Size是一一對應的,前者指序號 0、1、2、3,后者指具體對象大小 0B、8B、16B、24B
//go:notinheap
type mspan struct {
...
spanclass spanClass // size class and noscan (uint8)
...
}Go 語言的內存管理模塊中一共包含 67 種跨度類,每一個跨度類都會存儲特定大小的對象并且包含特定數量的頁數以及對象,所有的數據都會被預選計算好并存儲在runtime.class_to_size和runtime.class_to_allocnpages等變量中:
| class | bytes/obj | bytes/span | objects | tail waste | max waste |
|---|---|---|---|---|---|
| 1 | 8 | 8192 | 1024 | 0 | 87.50% |
| 2 | 16 | 8192 | 512 | 0 | 43.75% |
| 3 | 24 | 8192 | 341 | 0 | 29.24% |
| 4 | 32 | 8192 | 256 | 0 | 46.88% |
| 5 | 48 | 8192 | 170 | 32 | 31.52% |
| 6 | 64 | 8192 | 128 | 0 | 23.44% |
| 7 | 80 | 8192 | 102 | 32 | 19.07% |
| … | … | … | … | … | … |
| 67 | 32768 | 32768 | 1 | 0 | 12.50% |
上表展示了對象大小從 8B 到 32KB,總共 67 種跨度類的大小、存儲的對象數以及浪費的內存空間,以表中的第四個跨度類為例,跨度類為 5 的runtime.mspan中對象的大小上限為 48 字節(jié)、管理 1 個頁、最多可以存儲 170 個對象。因為內存需要按照頁進行管理,所以在尾部會浪費 32 字節(jié)的內存,當頁中存儲的對象都是 33 字節(jié)時,最多會浪費 31.52% 的資源:
((48−33)∗170+32)/8192=0.31518

除了上述 67 個跨度類之外,運行時中還包含 ID 為 0 的特殊跨度類,它能夠管理大于 32KB 的特殊對象。
1.2 線程緩存(mcache)
runtime.mcache是Go語言中的線程緩存,它會與線程上的處理器意義綁定,用于緩存用戶程序申請的微小對象。每一個線程緩存都持有numSpanClasses個(68∗2)個mspan,存儲在mcache的alloc字段中:
//go:notinheap
type mcache struct {
...
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
...
}1.3 中心緩存(mcentral)
每個中心緩存都會管理某個跨度類的內存管理單元,它會同時持有兩個runtime.spanSet,分別存儲包含空閑對象和不包含空閑對象的內存管理單元,訪問中心緩存中的內存管理單元需要使用互斥鎖。

如圖上所示,是 runtime.mcentral 中的 spanSet 的內存結構,index 字段是一個uint64類型數字的地址,該uint64的數字按32位分為前后兩半部分head和tail,向spanSet中插入和獲取mspan有其提供的push和pop函數,以push函數為例,會根據index的head,對spanSetBlock數據塊包含的mspan的個數512取商,得到spanSetBlock數據塊所在的地址,然后head對512取余,得到要插入的mspan在該spanSetBlock數據塊的具體地址。之所以是512,因為spanSet指向的spanSetBlock數據塊是一個包含512個mspan的集合。
由全部spanClass規(guī)格的runtime.mcentral共同組成的緩存結構如下:

1.4 頁堆(mheap)
//go:notinheap
type mheap struct {
...
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
...
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
...
}runtime.mheap是內存分配的核心結構體,其最重要的兩個字段如上。
在Go中其被作為全局變量mheap_存儲:
var mheap_ mheap
頁堆中包含一個長度為numSpanClasses個(68∗2)個的runtime.mcentral數組,其中 68 個為跨度類需要 scan 的中心緩存,另外的 68 個是 noscan (沒有指針,無需掃描)的中心緩存。
arenas是heapArena的二維數組的集合。如下:

2. 內存分配
堆上所有的對象內存分配都會通過runtime.newobject進行分配,運行時根據對象大小將它們分為微對象、小對象和大對象:
- 微對象(0, 16B):先使用微型分配器,再依次嘗試線程緩存、中心緩存和堆分配內存;多個小于16B的無指針微對象的內存分配請求,會合并向Tiny微對象空間申請,微對象的 16B 內存空間從 spanClass 為 4 或 5(無GC掃描)的mspan中獲取。
- 小對象[16B, 32KB]:先向mcache申請,mcache內存空間不夠時,向mcentral申請,mcentral不夠,則向頁堆mheap申請,再不夠就向操作系統申請。
- 大對象(32KB, +∞):大對象直接向頁堆mheap申請。
對于內存的釋放,遵循逐級釋放的策略。當ThreadCache的緩存充足或者過多時,則會將內存退還給CentralCache。當CentralCache內存過多或者充足,則將低命中內存塊退還PageHeap。
以上就是Golang內存管理之內存分配器詳解的詳細內容,更多關于Golang內存分配器的資料請關注腳本之家其它相關文章!
相關文章
詳解Golang中創(chuàng)建error的方式總結與應用場景
Golang中創(chuàng)建error的方式包括errors.New、fmt.Errorf、自定義實現了error接口的類型等,本文主要為大家介紹了這些方式的具體應用場景,需要的可以參考一下2023-07-07

