詳解Go是如何優(yōu)雅的進(jìn)行內(nèi)存管理
前言
Go語(yǔ)言拋棄C/C++中的開發(fā)者管理內(nèi)存的方式,實(shí)現(xiàn)了主動(dòng)申請(qǐng)與主動(dòng)釋放管理,增加了逃逸分析和垃圾回收,將開發(fā)者從內(nèi)存管理中釋放出來。
所以我們?cè)谌粘>帉懘a的時(shí)候不需要精通內(nèi)存的管理,它確實(shí)很復(fù)雜。但是另一方面,如果你掌握了Go內(nèi)存管理的基本概念和知識(shí)點(diǎn),可以讓你寫出更高質(zhì)量的,更壓榨機(jī)器性能的代碼;另外,還能幫助你更快更精準(zhǔn)得定位Bug,快速解決問題。所以,作為進(jìn)階的Go開發(fā),了解掌握Go的內(nèi)存管理還是很有必要的。
相關(guān)背景
存儲(chǔ)金字塔
馮·諾依曼計(jì)算機(jī)體系中的存儲(chǔ)器,是用于存儲(chǔ)程序和數(shù)據(jù)的?,F(xiàn)代計(jì)算機(jī)系統(tǒng)中,一般都是采用“CPU寄存器-CPU高速緩存-內(nèi)存-硬盤”的存儲(chǔ)器結(jié)構(gòu)。自上而下容量逐漸增大,速度逐漸減慢,單位價(jià)格逐漸降低。
1、CPU寄存器:存儲(chǔ)CPU正在使用的數(shù)據(jù)或指令。
2、CPU高速緩存:存儲(chǔ)CPU近期要用到的數(shù)據(jù)和指令。
3、內(nèi)存:存儲(chǔ)正在運(yùn)行或者將要運(yùn)行的程序和數(shù)據(jù)。
4、硬盤:存儲(chǔ)暫時(shí)不使用或者不能直接使用的程序和數(shù)據(jù)
虛擬內(nèi)存
物理內(nèi)存:是指實(shí)際通過物理內(nèi)存而獲得的內(nèi)存空間。
虛擬內(nèi)存:與物理內(nèi)存相反,是指根據(jù)系統(tǒng)需要從硬盤中虛擬的劃出一部分存儲(chǔ)空間。
虛擬內(nèi)存技術(shù)就是對(duì)內(nèi)存的一種抽象,有了這層抽象之后,程序運(yùn)行進(jìn)程的總大小可以超過實(shí)際可用的物理內(nèi)存大小。每個(gè)進(jìn)程都有自己的獨(dú)立虛擬地址空間,然后通過CPU和MMU把虛擬內(nèi)存地址轉(zhuǎn)換為實(shí)際物理地址。
TCMalloc
TCMalloc全稱是Thread Cache Malloc,是google為C語(yǔ)言開發(fā)的內(nèi)存分配算法,是Go內(nèi)存分配的起源。
TCMalloc內(nèi)存分配算法的核心思想是把內(nèi)存分為多級(jí)管理,從而降低鎖的粒度,它將可用的堆內(nèi)存采用二級(jí)分配的方式進(jìn)行管理,每個(gè)線程都會(huì)自行維護(hù)一個(gè)獨(dú)立的線程內(nèi)存池,進(jìn)行內(nèi)存分配時(shí)優(yōu)先從該線程內(nèi)存池中分配, 當(dāng)線程內(nèi)存池不足時(shí)才會(huì)向全局內(nèi)存池申請(qǐng),以避免不同線程對(duì)全局內(nèi)存池的頻繁競(jìng)爭(zhēng) ,進(jìn)一步的降低了內(nèi)存并發(fā)訪問的粒度。
Go的內(nèi)存分配算法是基于TCMalloc內(nèi)存分配算法實(shí)現(xiàn)的,借鑒了TCmalloc的思想。
幾個(gè)重要概念
Page: 操作系統(tǒng)對(duì)內(nèi)存的管理同樣是以頁(yè)為單位,但TCMalloc中的Page和操作系統(tǒng)的中頁(yè)是倍數(shù)關(guān)系,x64下Page大小為8KB。
Span:一組連續(xù)的Page被叫做Span,是TCMalloc內(nèi)存管理的基本單位,有不同大小的Span,比如2個(gè)Page大的Span,16個(gè)Page大的Span。
ThreadCache:每個(gè)線程各自的Cache,每個(gè)ThreadCache包含多個(gè)不同規(guī)格的Span鏈表,叫做SpanList,內(nèi)存分配的時(shí)候,可以根據(jù)要分配的內(nèi)存大小,快速選擇不同大小的SpanList,在SpanList上選擇合適的Span,每個(gè)線程都有自己的ThreadCache,所以ThreadCache是無鎖訪問的。
CentralCache:中心Cache,所有線程共享的Cache,也是保存的SpanList,數(shù)量和ThreadCache中數(shù)量相同,當(dāng)ThreadCache中內(nèi)存不足時(shí),可以從CentralCache中獲取,當(dāng)ThreadCache中內(nèi)存太多時(shí),可以放回CentralCache,由于CentralCache是線程共享的,所以它的訪問需要加鎖。
PageHeap:堆內(nèi)存的抽象,同樣當(dāng)CentealCache中內(nèi)存太多或太少時(shí),都可從PageHeap中放回或獲取,同樣,PageHeap的訪問也是需要加鎖的。
管理分配
核心思想
Go在程序啟動(dòng)的時(shí)候,會(huì)分配一塊連續(xù)的內(nèi)存(虛擬的地址空間,還沒有真正地分配內(nèi)存),切成小塊后自己進(jìn)行管理,對(duì)內(nèi)存的分配遵循以下思想。
1.每次從操作系統(tǒng)申請(qǐng)一大塊內(nèi)存, 以減少系統(tǒng)調(diào)用。
2.將申請(qǐng)到的大塊內(nèi)存按照特定大小預(yù)先切分成小塊, 構(gòu)成鏈表。
3.為對(duì)象分配內(nèi)存時(shí), 只需從大小合適的鏈表提取一個(gè)小塊即可。
4.回收對(duì)象內(nèi)存時(shí), 將該小塊內(nèi)存重新歸還到原鏈表, 以便復(fù)用。
5.如閑置內(nèi)存過多, 則嘗試歸還部分內(nèi)存給操作系統(tǒng), 降低整體開銷。
內(nèi)存管理由mcache、mcentral、mheap組成一個(gè)三級(jí)管理結(jié)構(gòu),本質(zhì)上都是對(duì)mspan的管理,三者用于不同的目的來共同配合管理所有mspan。
mspan
mspan是Go中內(nèi)存管理的基本單元,是由一片連續(xù)的8kB的page組成的內(nèi)存塊。但是小對(duì)象和大對(duì)象分配的位置不用,大對(duì)象在mheap上分配,mheap向操作系統(tǒng)申請(qǐng)新內(nèi)存時(shí),是向虛擬內(nèi)存申請(qǐng);小對(duì)象使用mcache的tiny分配器分配。
一組連續(xù)的Page組成1個(gè)Span,go把內(nèi)存分為67個(gè)大小不同的span,并且大小是不固定的。
源碼文件src/runtime/sizeclasses.go對(duì)67種span的定義(源碼版本為go-1.17.1,本文下所有源碼展示均為此版本)
延伸擴(kuò)展:67種定義列表里面有一列的名稱叫做"max waste",代表的是這個(gè)span下可能出現(xiàn)的最大內(nèi)存浪費(fèi)比例。舉個(gè)例子解釋,看第4個(gè)規(guī)格的情況:
class bytes/obj bytes/span objects tail waste max waste min align 4 32 8192 256 0 21.88% 32
4的對(duì)象最小內(nèi)存長(zhǎng)度為25字節(jié)(因?yàn)樾∮?5的只會(huì)申請(qǐng)3或3以下的,要不到4),所以如果每個(gè)object都被25字節(jié)的對(duì)象申請(qǐng),此時(shí)內(nèi)存浪費(fèi)最大,對(duì)應(yīng)浪費(fèi)率為:(32-25)/32 = 21.88%。
再通過觀察整個(gè)列表可以看到,"max waste"一列并非線性遞減的,熟悉Linux的同學(xué)應(yīng)該猜到原因了,沒錯(cuò),這個(gè)設(shè)計(jì)跟大名鼎鼎的伙伴算法是非常相似的。
伙伴算法(buddy算法),就是將內(nèi)存分成若干塊,然后以最適合的方式滿足程序內(nèi)存需求的一種內(nèi)存管理算法,伙伴算法是盡可能地在提高內(nèi)存利用率的同時(shí)減少內(nèi)存碎片。但是算法中,一個(gè)很小的塊往往會(huì)阻礙一個(gè)大塊的合并,一個(gè)系統(tǒng)中,對(duì)內(nèi)存塊的分配,大小是隨機(jī)的,一片內(nèi)存中僅一個(gè)小的內(nèi)存塊沒有釋放,旁邊兩個(gè)大的就不能合并,這也是造成上面現(xiàn)象的根因。(完整解讀伙伴算法需要非常大的篇幅和難度,本文就不展開了,文章最后有參考鏈接,讀者可自行研究)
回來主題,上面說到的Spans有3種類型:
空閑-span,沒有對(duì)象,可以釋放回操作系統(tǒng),或重用于堆分配,或重用于堆棧內(nèi)存。
正在使用-span,至少有一個(gè)堆對(duì)象,可能有更多的空間。
棧-span,用于 goroutine 堆棧。此跨度可以存在于堆棧中或堆中,但不能同時(shí)存在。
源碼文件src/runtime/mheap.go對(duì)mspan結(jié)構(gòu)體的定義
type mspan struct { next *mspan // 鏈表后向指針 prev *mspan // 鏈表前向指針 list *mSpanList // 雙端隊(duì)列的head(已無實(shí)際用途) startAddr uintptr // span起始位置的地址指針 npages uintptr // 可供分配的頁(yè)數(shù) ... manualFreeList gclinkptr // 在mSpanManual的空閑對(duì)象 allocCache uint64 // 在freeindex處的allocBits的緩存 ... allocBits *gcBits // 標(biāo)記span中的elem哪些是被使用的,哪些是未被使用的 gcmarkBits *gcBits // 標(biāo)記span中的elem哪些是被標(biāo)記的,哪些是未被標(biāo)記的 speciallock mutex // 互斥鎖 }
管理組件說明
內(nèi)存管理器由mcache, mcentral, mheap3種組件構(gòu)成: 三級(jí)管理結(jié)構(gòu)是為了方便對(duì)span進(jìn)行管理,加速對(duì)span對(duì)象的訪問和分配,這三個(gè)結(jié)構(gòu)在runtime中分別有對(duì)應(yīng)的mcache.go、mcentral.go、mheap.go文件。
mcache:保存的是各種大小的Span,并按Span class分類,小對(duì)象直接從mcache分配內(nèi)存,它起到了緩存的作用,并且可以無鎖訪問Go中是每個(gè)P擁有1個(gè)mcache,因?yàn)樵贕o程序中,當(dāng)前最多有GOMAXPROCS個(gè)線程在運(yùn)行,所以最多需要GOMAXPROCS個(gè)mcache就可以保證各線程對(duì)mcache的無鎖訪問。
mcentral:是所有線程共享的緩存,需要加鎖訪問,它按Span class對(duì)Span分類,串聯(lián)成鏈表,當(dāng)mcache的某個(gè)級(jí)別Span的內(nèi)存被分配光時(shí),它會(huì)向mcentral申請(qǐng)1個(gè)當(dāng)前級(jí)別的Span。
mheap:是堆內(nèi)存的抽象,把從OS(系統(tǒng))申請(qǐng)出的內(nèi)存頁(yè)組織成Span,并保存起來。當(dāng)mcentral的Span不夠用時(shí)會(huì)向mheap申請(qǐng),mheap的Span不夠用時(shí)會(huì)向OS申請(qǐng),向OS的內(nèi)存申請(qǐng)是按頁(yè)來的,然后把申請(qǐng)來的內(nèi)存頁(yè)生成Span組織起來,同樣也是需要加鎖訪問的。mheap主要用于大對(duì)象的內(nèi)存分配,以及管理未切割的mspan,用于給mcentral切割成小對(duì)象。
熟悉的金字塔,熟悉的結(jié)構(gòu)
通俗的理解:mcache, mcentral, mheap就是對(duì)ThreadCache, CentralCache, PageHeap的繼承沿用和基于go體系的優(yōu)化處理版本。
分配流程
Go的內(nèi)存分配器在分配對(duì)象時(shí),根據(jù)對(duì)象的大小,分成三類:小對(duì)象(<=16B)、一般對(duì)象(>16B && <=32KB)、大對(duì)象(>32KB)。
源碼文件src/runtime/malloc.go根據(jù)分配對(duì)象的大小選擇對(duì)應(yīng)的空間申請(qǐng)
大體上的分配流程:
1.>32KB 的對(duì)象,直接從mheap上分配。
2.<=16B 的對(duì)象使用mcache的tiny分配器分配。
3.>16B && <=32KB 的對(duì)象,首先計(jì)算對(duì)象的規(guī)格大小,然后使用mcache中相應(yīng)規(guī)格大小的mspan分配。
如果mcache沒有相應(yīng)規(guī)格大小的mspan,則向mcentral申請(qǐng); 如果mcentral沒有相應(yīng)規(guī)格大小的mspan,則向mheap申請(qǐng); 如果mheap中也沒有合適大小的mspan,則向OS申請(qǐng)。
源碼文件src/runtime/mheap.go內(nèi)存分配初始化過程
小結(jié)
Go內(nèi)存管理源自TCMalloc,優(yōu)秀作品源于繼承和優(yōu)化(在這里我自己想到了一句話:如果說我比別人看得更遠(yuǎn)些,那是因?yàn)槲艺驹诹司奕说募缟?-牛頓)。但它比TCMalloc還多了2件東西:逃逸分析(后面篇幅會(huì)提及)和垃圾回收。
總結(jié)一下它在底層設(shè)計(jì)上著重用到的2個(gè)重要觀念:
使用緩存提高效率:在存儲(chǔ)的整個(gè)體系中到處可見緩存的思想,利用緩存減少了系統(tǒng)調(diào)用的次數(shù),降低了鎖的粒度、減少加鎖的次數(shù),提高了管理效率。
以空間換時(shí)間:空間換時(shí)間是一種常用的性能優(yōu)化思想,數(shù)據(jù)庫(kù)的索引/許多數(shù)據(jù)結(jié)構(gòu)的本質(zhì)就是空間換時(shí)間。
關(guān)聯(lián)知識(shí)點(diǎn)
逃逸分析
Go堆內(nèi)存所使用的內(nèi)存頁(yè)與goroutine的棧所使用的內(nèi)存頁(yè)是交織在一起的,帶GC(垃圾回收)功能的GO語(yǔ)言會(huì)對(duì)位于堆上的對(duì)象進(jìn)行自動(dòng)管理。當(dāng)某個(gè)對(duì)象不可達(dá)時(shí),即沒有其對(duì)象引用它時(shí),它將會(huì)被回收并被重用(三色標(biāo)記)。但GC是會(huì)給程序帶來性能損耗的,尤其是當(dāng)堆內(nèi)存上有大量待掃描的堆內(nèi)存對(duì)象時(shí),將會(huì)給GC帶來過大的壓力,從而消耗更多的計(jì)算和存儲(chǔ)資源。于是開發(fā)者們都想盡量減少在堆上的內(nèi)存分配,可以在棧上分配的變量盡量留在棧上。
逃逸分析(escape analysis)就是在程序編譯階段根據(jù)程序代碼中的數(shù)據(jù)流,對(duì)代碼中哪些變量需要在棧上分配,哪些變量需要在堆上分配進(jìn)行靜態(tài)分析的方法。
分析準(zhǔn)則: 逃逸分析是在編譯器完成的,也就是只存在于編譯階段;如果變量在函數(shù)外部沒有引用,則優(yōu)先放到棧中;如果變量在函數(shù)外部存在引用,則必定放在堆中。
命令:go build -gcflags '-m -m -l' xxx.go
內(nèi)存對(duì)齊
CPU訪問內(nèi)存時(shí),并不是逐個(gè)字節(jié)訪問,而是以字長(zhǎng)為單位訪問。這樣是為了是減少CPU訪問內(nèi)存的次數(shù),提升CPU訪問內(nèi)存的吞吐量。如果訪問對(duì)象在內(nèi)存的存儲(chǔ)空間是對(duì)齊的話,CPU讀取一次即可,否則就要讀取兩次甚至多次,如下圖清晰可見。
對(duì)齊規(guī)則
1.第一個(gè)成員在與結(jié)構(gòu)體變量偏移量為0的地址處;2.其他成員變量要對(duì)齊到對(duì)齊數(shù)的整數(shù)倍的地址處;3.結(jié)構(gòu)體總大小為最大對(duì)齊數(shù)的整數(shù)倍。
下圖是不同類型的對(duì)齊系數(shù)和占用字節(jié)數(shù)
所以我們?cè)谌粘>幋a過程中,要盡量對(duì)結(jié)構(gòu)體的變量類型做針對(duì)性的順序調(diào)整,以符合對(duì)齊原則。
One More Thing
介紹完了Go的情況,最后來簡(jiǎn)單看下其他語(yǔ)言的,作者本人對(duì)Java不太熟悉,就不摻和了,就用最簡(jiǎn)單的描述來講一下相對(duì)熟悉的php和python。
php:php也是有一個(gè)基本的分配單元叫chunk,chunk分配了512個(gè)page,page的大小為4KB。內(nèi)存分配模式也是有3種,small(小于等于3KB),large(大于3KB小于等于2MB-4KB內(nèi)存),huge(大于2MB-4KB內(nèi)存),GC機(jī)制是引用計(jì)數(shù)方式,對(duì)堆區(qū)zend_mm_heap的管控就相對(duì)非常隨意了(純個(gè)人心得理解)。
python:py最大的特色是有個(gè)內(nèi)存池,可以減少內(nèi)存碎片化,提高執(zhí)行效率?;厥諜C(jī)制也是引用計(jì)數(shù),但是它有標(biāo)記/清除和分代回收兩個(gè)輔助功能。
綜合3種語(yǔ)言對(duì)比,可以看到既有共同交集的地方,也有各自的私有屬性特色,各自的管理分配方式用到自己的語(yǔ)言環(huán)境下都能發(fā)揮最大的作用和效率。這也驗(yàn)證了一句至高的哲學(xué):方案設(shè)計(jì)或者架構(gòu)理念,是沒有最優(yōu)秀最完美的,但是會(huì)有最適合最貼近使用場(chǎng)景的。
以上就是詳解Go是如何優(yōu)雅進(jìn)行內(nèi)存管理的詳細(xì)內(nèi)容,更多關(guān)于Go內(nèi)存管理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文詳解Go語(yǔ)言中的Option設(shè)計(jì)模式
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中Option設(shè)計(jì)模式的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解一下2023-05-05go語(yǔ)言區(qū)塊鏈學(xué)習(xí)調(diào)用以太坊
這篇文章主要為大家介紹了go語(yǔ)言區(qū)塊鏈學(xué)習(xí)如何調(diào)用以太坊的示例實(shí)現(xiàn)過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10Golang如何快速構(gòu)建一個(gè)CLI小工具詳解
這篇文章主要為大家介紹了Golang如何快速構(gòu)建一個(gè)CLI小工具詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11go json數(shù)據(jù)轉(zhuǎn)發(fā)的實(shí)現(xiàn)代碼
這篇文章主要介紹了go json數(shù)據(jù)轉(zhuǎn)發(fā)的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09