Golang內(nèi)存管理之內(nèi)存分配器詳解
0. 簡介
程序中的數(shù)據(jù)都會(huì)被分配到程序所在的虛擬內(nèi)存中,內(nèi)存空間包含兩個(gè)重要區(qū)域:棧(Stack) 和 堆(Heap)。函數(shù)調(diào)用的參數(shù)、返回值和局部變量大部分會(huì)分配在棧上,這部分由編譯器管理。堆內(nèi)存的管理方式視語言而定:
- C/C++等編程語言的堆內(nèi)存由工程師主動(dòng)申請和釋放;
- Go、Java等編程語言由工程師和編譯器/運(yùn)行時(shí)共同管理,其內(nèi)存由內(nèi)存分配器分配,由垃圾回收器回收。
本文就介紹一下Go語言的內(nèi)存分配器。
1. Go內(nèi)存分配設(shè)計(jì)原理
Go內(nèi)存分配器的設(shè)計(jì)思想來源于TCMalloc,全稱是Thread-Caching Malloc,核心思想是把內(nèi)存分為多級管理,利用緩存的思想提升內(nèi)存使用效率,降低鎖的粒度。
在堆內(nèi)存管理上分為三個(gè)內(nèi)存級別:
- 線程緩存(MCache):作為線程獨(dú)立的內(nèi)存池,與線程的第一交互內(nèi)存,訪問無需加鎖;
- 中心緩存(MCentral):作為線程緩存的下一級,是多個(gè)線程共享的,所以訪問時(shí)需要加鎖;
- 頁堆(MHeap):中心緩存的下一級,在遇到32KB以上的對象時(shí),會(huì)直接選擇頁堆分配大內(nèi)存,而當(dāng)頁堆內(nèi)存不夠時(shí),則會(huì)通過系統(tǒng)調(diào)用向系統(tǒng)申請內(nèi)存。
1.1 內(nèi)存管理基本單元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內(nèi)存管理的基本單元,其結(jié)構(gòu)體中包含的next和prev指針,分別指向前后的runtime.mspan,所以其串聯(lián)后的結(jié)構(gòu)是一個(gè)雙向鏈表。
而startAddr表示此mspan的起始地址,npages表示管理的頁數(shù),每頁大小8KB,這個(gè)頁不是操作系統(tǒng)的內(nèi)存頁,一般是操作系統(tǒng)內(nèi)存頁的整數(shù)倍。
其它字段:
freeindex— 掃描頁中空閑對象的初始索引;allocBits和gcmarkBits— 分別用于標(biāo)記內(nèi)存的占用和回收情況;allocCache—allocBits的補(bǔ)碼,可以用于快速查找內(nèi)存中未被使用的內(nèi)存;
注意使用//go:notinheap標(biāo)記次結(jié)構(gòu)體mspan為非堆上類型,保證此類型對象不會(huì)逃逸到堆上。
圖示:

跨度類
在mspan中有一個(gè)字段是spanclass,稱為跨度類,是對mspan大小級別的劃分,每個(gè)mspan能夠存放指定范圍大小的對象,32KB以內(nèi)的小對象在Go中,會(huì)對應(yīng)不同大小的內(nèi)存刻度Size Class,Size Class和Object Size是一一對應(yīng)的,前者指序號(hào) 0、1、2、3,后者指具體對象大小 0B、8B、16B、24B
//go:notinheap
type mspan struct {
...
spanclass spanClass // size class and noscan (uint8)
...
}Go 語言的內(nèi)存管理模塊中一共包含 67 種跨度類,每一個(gè)跨度類都會(huì)存儲(chǔ)特定大小的對象并且包含特定數(shù)量的頁數(shù)以及對象,所有的數(shù)據(jù)都會(huì)被預(yù)選計(jì)算好并存儲(chǔ)在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 種跨度類的大小、存儲(chǔ)的對象數(shù)以及浪費(fèi)的內(nèi)存空間,以表中的第四個(gè)跨度類為例,跨度類為 5 的runtime.mspan中對象的大小上限為 48 字節(jié)、管理 1 個(gè)頁、最多可以存儲(chǔ) 170 個(gè)對象。因?yàn)閮?nèi)存需要按照頁進(jìn)行管理,所以在尾部會(huì)浪費(fèi) 32 字節(jié)的內(nèi)存,當(dāng)頁中存儲(chǔ)的對象都是 33 字節(jié)時(shí),最多會(huì)浪費(fèi) 31.52% 的資源:
((48−33)∗170+32)/8192=0.31518

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

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

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是內(nèi)存分配的核心結(jié)構(gòu)體,其最重要的兩個(gè)字段如上。
在Go中其被作為全局變量mheap_存儲(chǔ):
var mheap_ mheap
頁堆中包含一個(gè)長度為numSpanClasses個(gè)(68∗2)個(gè)的runtime.mcentral數(shù)組,其中 68 個(gè)為跨度類需要 scan 的中心緩存,另外的 68 個(gè)是 noscan (沒有指針,無需掃描)的中心緩存。
arenas是heapArena的二維數(shù)組的集合。如下:

2. 內(nèi)存分配
堆上所有的對象內(nèi)存分配都會(huì)通過runtime.newobject進(jìn)行分配,運(yùn)行時(shí)根據(jù)對象大小將它們分為微對象、小對象和大對象:
- 微對象(0, 16B):先使用微型分配器,再依次嘗試線程緩存、中心緩存和堆分配內(nèi)存;多個(gè)小于16B的無指針微對象的內(nèi)存分配請求,會(huì)合并向Tiny微對象空間申請,微對象的 16B 內(nèi)存空間從 spanClass 為 4 或 5(無GC掃描)的mspan中獲取。
- 小對象[16B, 32KB]:先向mcache申請,mcache內(nèi)存空間不夠時(shí),向mcentral申請,mcentral不夠,則向頁堆mheap申請,再不夠就向操作系統(tǒng)申請。
- 大對象(32KB, +∞):大對象直接向頁堆mheap申請。
對于內(nèi)存的釋放,遵循逐級釋放的策略。當(dāng)ThreadCache的緩存充足或者過多時(shí),則會(huì)將內(nèi)存退還給CentralCache。當(dāng)CentralCache內(nèi)存過多或者充足,則將低命中內(nèi)存塊退還PageHeap。
以上就是Golang內(nèi)存管理之內(nèi)存分配器詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang內(nèi)存分配器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
自動(dòng)生成代碼controller?tool的簡單使用
這篇文章主要為大家介紹了自動(dòng)生成代碼controller?tool的簡單使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
golang解析json數(shù)據(jù)的4種方法總結(jié)
在日常工作中每一名開發(fā)者,不管是前端還是后端,都經(jīng)常使用 JSON,下面這篇文章主要給大家介紹了關(guān)于golang解析json數(shù)據(jù)的4種方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06
詳解Golang中創(chuàng)建error的方式總結(jié)與應(yīng)用場景
Golang中創(chuàng)建error的方式包括errors.New、fmt.Errorf、自定義實(shí)現(xiàn)了error接口的類型等,本文主要為大家介紹了這些方式的具體應(yīng)用場景,需要的可以參考一下2023-07-07
基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解
這篇文章主要為大家介紹了基于GORM實(shí)現(xiàn)CreateOrUpdate方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10

