Golang內(nèi)存管理之內(nèi)存分配器詳解
0. 簡介
程序中的數(shù)據(jù)都會被分配到程序所在的虛擬內(nèi)存中,內(nèi)存空間包含兩個重要區(qū)域:棧(Stack) 和 堆(Heap)。函數(shù)調(diào)用的參數(shù)、返回值和局部變量大部分會分配在棧上,這部分由編譯器管理。堆內(nèi)存的管理方式視語言而定:
- C/C++等編程語言的堆內(nèi)存由工程師主動申請和釋放;
- Go、Java等編程語言由工程師和編譯器/運行時共同管理,其內(nèi)存由內(nèi)存分配器分配,由垃圾回收器回收。
本文就介紹一下Go語言的內(nèi)存分配器。
1. Go內(nèi)存分配設(shè)計原理
Go內(nèi)存分配器的設(shè)計思想來源于TCMalloc
,全稱是Thread-Caching Malloc
,核心思想是把內(nèi)存分為多級管理,利用緩存的思想提升內(nèi)存使用效率,降低鎖的粒度。
在堆內(nèi)存管理上分為三個內(nèi)存級別:
- 線程緩存(MCache):作為線程獨立的內(nèi)存池,與線程的第一交互內(nèi)存,訪問無需加鎖;
- 中心緩存(MCentral):作為線程緩存的下一級,是多個線程共享的,所以訪問時需要加鎖;
- 頁堆(MHeap):中心緩存的下一級,在遇到32KB以上的對象時,會直接選擇頁堆分配大內(nèi)存,而當頁堆內(nèi)存不夠時,則會通過系統(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)是一個雙向鏈表。
而startAddr
表示此mspan
的起始地址,npages
表示管理的頁數(shù),每頁大小8KB,這個頁不是操作系統(tǒng)的內(nèi)存頁,一般是操作系統(tǒng)內(nèi)存頁的整數(shù)倍。
其它字段:
freeindex
— 掃描頁中空閑對象的初始索引;allocBits
和gcmarkBits
— 分別用于標記內(nèi)存的占用和回收情況;allocCache
—allocBits
的補碼,可以用于快速查找內(nèi)存中未被使用的內(nèi)存;
注意使用//go:notinheap
標記次結(jié)構(gòu)體mspan
為非堆上類型,保證此類型對象不會逃逸到堆上。
圖示:
跨度類
在mspan
中有一個字段是spanclass
,稱為跨度類,是對mspan大小級別的劃分,每個mspan能夠存放指定范圍大小的對象,32KB以內(nèi)的小對象在Go中,會對應(yīng)不同大小的內(nèi)存刻度Size Class,Size Class和Object Size是一一對應(yīng)的,前者指序號 0、1、2、3,后者指具體對象大小 0B、8B、16B、24B
//go:notinheap type mspan struct { ... spanclass spanClass // size class and noscan (uint8) ... }
Go 語言的內(nèi)存管理模塊中一共包含 67 種跨度類,每一個跨度類都會存儲特定大小的對象并且包含特定數(shù)量的頁數(shù)以及對象,所有的數(shù)據(jù)都會被預(yù)選計算好并存儲在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 種跨度類的大小、存儲的對象數(shù)以及浪費的內(nèi)存空間,以表中的第四個跨度類為例,跨度類為 5 的runtime.mspan
中對象的大小上限為 48 字節(jié)、管理 1 個頁、最多可以存儲 170 個對象。因為內(nèi)存需要按照頁進行管理,所以在尾部會浪費 32 字節(jié)的內(nèi)存,當頁中存儲的對象都是 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)
每個中心緩存都會管理某個跨度類的內(nèi)存管理單元,它會同時持有兩個runtime.spanSet
,分別存儲包含空閑對象和不包含空閑對象的內(nèi)存管理單元,訪問中心緩存中的內(nèi)存管理單元需要使用互斥鎖。
如圖上所示,是 runtime.mcentral 中的 spanSet 的內(nèi)存結(jié)構(gòu),index 字段是一個uint64類型數(shù)字的地址,該uint64的數(shù)字按32位分為前后兩半部分head和tail,向spanSet中插入和獲取mspan有其提供的push和pop函數(shù),以push函數(shù)為例,會根據(jù)index的head,對spanSetBlock數(shù)據(jù)塊包含的mspan的個數(shù)512取商,得到spanSetBlock數(shù)據(jù)塊所在的地址,然后head對512取余,得到要插入的mspan在該spanSetBlock數(shù)據(jù)塊的具體地址。之所以是512,因為spanSet指向的spanSetBlock數(shù)據(jù)塊是一個包含512個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)體,其最重要的兩個字段如上。
在Go中其被作為全局變量mheap_
存儲:
var mheap_ mheap
頁堆中包含一個長度為numSpanClasses
個(68∗2)個的runtime.mcentral
數(shù)組,其中 68 個為跨度類需要 scan
的中心緩存,另外的 68 個是 noscan
(沒有指針,無需掃描)的中心緩存。
arenas是heapArena的二維數(shù)組的集合。如下:
2. 內(nèi)存分配
堆上所有的對象內(nèi)存分配都會通過runtime.newobject
進行分配,運行時根據(jù)對象大小將它們分為微對象、小對象和大對象:
- 微對象(0, 16B):先使用微型分配器,再依次嘗試線程緩存、中心緩存和堆分配內(nèi)存;多個小于16B的無指針微對象的內(nèi)存分配請求,會合并向Tiny微對象空間申請,微對象的 16B 內(nèi)存空間從 spanClass 為 4 或 5(無GC掃描)的mspan中獲取。
- 小對象[16B, 32KB]:先向mcache申請,mcache內(nèi)存空間不夠時,向mcentral申請,mcentral不夠,則向頁堆mheap申請,再不夠就向操作系統(tǒng)申請。
- 大對象(32KB, +∞):大對象直接向頁堆mheap申請。
對于內(nèi)存的釋放,遵循逐級釋放的策略。當ThreadCache的緩存充足或者過多時,則會將內(nèi)存退還給CentralCache。當CentralCache內(nèi)存過多或者充足,則將低命中內(nèi)存塊退還PageHeap。
以上就是Golang內(nèi)存管理之內(nèi)存分配器詳解的詳細內(nèi)容,更多關(guān)于Golang內(nèi)存分配器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang解析json數(shù)據(jù)的4種方法總結(jié)
在日常工作中每一名開發(fā)者,不管是前端還是后端,都經(jīng)常使用 JSON,下面這篇文章主要給大家介紹了關(guān)于golang解析json數(shù)據(jù)的4種方法,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-06-06詳解Golang中創(chuàng)建error的方式總結(jié)與應(yīng)用場景
Golang中創(chuàng)建error的方式包括errors.New、fmt.Errorf、自定義實現(xiàn)了error接口的類型等,本文主要為大家介紹了這些方式的具體應(yīng)用場景,需要的可以參考一下2023-07-07基于GORM實現(xiàn)CreateOrUpdate方法詳解
這篇文章主要為大家介紹了基于GORM實現(xiàn)CreateOrUpdate方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10