前言
本文主要記錄個(gè)人學(xué)習(xí)Golang堆內(nèi)存管理,涉及到的相關(guān)內(nèi)容广恢,算是對(duì)個(gè)人所學(xué)知識(shí)點(diǎn)的梳理與總結(jié)。從非常宏觀的角度看呀潭,Go的堆內(nèi)存管理就是下圖這個(gè)樣子
學(xué)習(xí)內(nèi)存管理钉迷,肯定首先需要了解內(nèi)存管理的基本知識(shí),我會(huì)按照內(nèi)存管理基礎(chǔ)知識(shí)-->TCMalloc-->Go堆內(nèi)存管理基礎(chǔ)概念-->Go堆內(nèi)存分配流程钠署,這樣的順序來(lái)逐步梳理相關(guān)知識(shí)糠聪。
內(nèi)存管理基礎(chǔ)知識(shí)
1. 存儲(chǔ)器與內(nèi)存
在計(jì)算機(jī)的組成結(jié)構(gòu)中有一個(gè)很重要的部分是存儲(chǔ)器。它是用來(lái)存儲(chǔ)程序和數(shù)據(jù)的部件谐鼎。對(duì)于計(jì)算機(jī)來(lái)說(shuō)舰蟆,有了存儲(chǔ)器,才有記憶功能狸棍,才能保證正常工作身害。存儲(chǔ)器的種類很多。按其用途可分為主存儲(chǔ)器(也稱為內(nèi)存儲(chǔ)器草戈,簡(jiǎn)稱內(nèi)存)和輔助存儲(chǔ)器(也稱為外存儲(chǔ)器)塌鸯。
外存儲(chǔ)器主要是指除計(jì)算機(jī)內(nèi)存及CPU緩存以外的儲(chǔ)存器,此類儲(chǔ)存器一般斷電后仍然能保存數(shù)據(jù)唐片。常見的外存儲(chǔ)器有硬盤丙猬、軟盤、光盤费韭、U盤等茧球。
內(nèi)存一般采用半導(dǎo)體存儲(chǔ)單元,包括隨機(jī)存儲(chǔ)器(RAM)星持,只讀存儲(chǔ)器(ROM)抢埋,以及高速緩存(CACHE)。
只讀存儲(chǔ)器 ROM(Read Only Memory)
只能讀出,一般不能寫入揪垄,即使機(jī)器停電鲤屡,這些數(shù)據(jù)也不會(huì)丟失。一般用于存放計(jì)算機(jī)的基本程序和數(shù)據(jù)福侈,如BIOS ROM酒来。-
隨機(jī)存儲(chǔ)器 RAM(Random Access Memory)
既可以從中讀取數(shù)據(jù),也可以寫入數(shù)據(jù)肪凛。當(dāng)機(jī)器電源關(guān)閉時(shí)堰汉,存于其中的數(shù)據(jù)就會(huì)丟失。
RAM分為兩種:動(dòng)態(tài)存儲(chǔ)芯片(DRAM)和靜態(tài)存儲(chǔ)芯片(SRAM)伟墙。- DRAM:DRAM結(jié)構(gòu)較簡(jiǎn)單且集成度高翘鸭,通常用于制造內(nèi)存條中的存儲(chǔ)芯片。
- SRAM:SRAM速度快且不需要刷新操作戳葵,但集成度差和功耗較大就乓,通常用于制造容量小但效率高的CPU緩存。
-
高速緩存 Cache
高速緩沖存儲(chǔ)器是存在于主存與CPU之間的一級(jí)存儲(chǔ)器拱烁, 由靜態(tài)存儲(chǔ)芯片(SRAM)組成生蚁,容量比較小但速度比主存高得多, 接近于CPU的速度戏自。由于從1980年開始CPU和內(nèi)存速率差距在不斷拉大邦投,為了彌補(bǔ)這2個(gè)硬件之間的速率差異,所以在CPU跟內(nèi)存之間增加了比內(nèi)存更快的Cache擅笔,Cache是內(nèi)存數(shù)據(jù)的緩存志衣,可以降低CPU訪問內(nèi)存的時(shí)間。三級(jí)Cache分別是L1猛们、L2念脯、L3,它們的速率是三個(gè)不同的層級(jí)弯淘,L1速率最快绿店,與CPU速率最接近,是RAM速率的100倍耳胎,L2速率就降到了RAM的25倍惯吕,L3的速率更靠近RAM的速率惕它。
寄存器是CPU內(nèi)部用來(lái)存放數(shù)據(jù)的一些小型存儲(chǔ)區(qū)域怕午,用來(lái)暫時(shí)存放參與運(yùn)算的數(shù)據(jù)和運(yùn)算結(jié)果。
那么當(dāng)CPU要去讀取來(lái)自遠(yuǎn)程網(wǎng)絡(luò)服務(wù)器上的磁盤文件時(shí)淹魄,就是由CPU直接和遠(yuǎn)程服務(wù)器磁盤交互嗎郁惜?事實(shí)當(dāng)然不是這樣的。由于CPU的執(zhí)行速率遠(yuǎn)遠(yuǎn)高于外部存儲(chǔ)的讀寫速率,所以當(dāng)CPU去讀取磁盤中數(shù)據(jù)時(shí)兆蕉,通常會(huì)先查看離自己最近的寄存器是否有緩存對(duì)應(yīng)的數(shù)據(jù)羽戒,如果存在想要的數(shù)據(jù)就會(huì)直接獲取。而寄存器的讀寫速率十分接近CPU虎韵,將數(shù)據(jù)緩存在寄存其中可以極大地提升執(zhí)行效率易稠,避免低效的磁盤讀寫降低性能。
由于計(jì)算機(jī)的存儲(chǔ)體系中包蓝,存儲(chǔ)量越大越低廉的存儲(chǔ)設(shè)備往往讀寫越慢驶社,存儲(chǔ)量越小越昂貴的存儲(chǔ)設(shè)備往往讀寫越快。而為了存儲(chǔ)更多的數(shù)據(jù)测萎,大量數(shù)據(jù)往往存儲(chǔ)在讀寫慢的存儲(chǔ)設(shè)備上亡电。為了讓CPU在執(zhí)行讀寫操作時(shí),執(zhí)行效率盡可能地不被讀寫慢的存儲(chǔ)設(shè)備影響硅瞧,于是下圖中的存儲(chǔ)器層次結(jié)構(gòu)便孕育而生了份乒。
存儲(chǔ)器層次結(jié)構(gòu)的主要思想,就是讓讀寫更快的存儲(chǔ)設(shè)備作為讀寫慢但容量更大的存儲(chǔ)器的高速緩存腕唧,讓CPU每次優(yōu)先訪問上層讀寫更快的設(shè)備或辖,盡量減少與低效存儲(chǔ)設(shè)備的讀寫交互,以保證計(jì)算機(jī)的整體性能枣接。
2. 虛擬內(nèi)存
2.1 為什么使用虛擬內(nèi)存
計(jì)算機(jī)對(duì)于內(nèi)存真正的載體是物理內(nèi)存條,這個(gè)是實(shí)打?qū)嵉奈锢碛布萘啃⒘瑁栽诓僮飨到y(tǒng)中定義這部份的容量叫物理內(nèi)存(主存)。物理內(nèi)存的布局實(shí)際上就是一個(gè)內(nèi)存大數(shù)組月腋,如圖所示蟀架。每一個(gè)元素都會(huì)對(duì)應(yīng)一個(gè)地址,稱之為物理內(nèi)存地址榆骚。那么CPU在運(yùn)算的過(guò)程中片拍,如果需要從內(nèi)存中取1個(gè)字節(jié)的數(shù)據(jù),就需要基于這個(gè)數(shù)據(jù)的物理內(nèi)存地址去運(yùn)算即可妓肢,而且物理內(nèi)存地址是連續(xù)的捌省,可以根據(jù)一個(gè)基準(zhǔn)地址進(jìn)行偏移來(lái)取得相應(yīng)的一塊連續(xù)內(nèi)存數(shù)據(jù)。
一個(gè)操作系統(tǒng)是不可能只運(yùn)行一個(gè)程序的碉钠,當(dāng)N個(gè)程序共同使用同一個(gè)物理內(nèi)存時(shí)纲缓,就會(huì)存在以下問題:
1. 內(nèi)存資源是稀缺的,每個(gè)進(jìn)程為了保證自己能夠運(yùn)行喊废,會(huì)為自己申請(qǐng)額外大的內(nèi)存祝高,導(dǎo)致空閑內(nèi)存被浪費(fèi)
2. 物理內(nèi)存對(duì)所有進(jìn)程是共享的,多進(jìn)程同時(shí)訪問同一個(gè)物理內(nèi)存會(huì)存在并發(fā)問題
為了解決以上問題污筷,操作系統(tǒng)便引入了虛擬內(nèi)存工闺。通過(guò)虛擬內(nèi)存作為物理內(nèi)存和進(jìn)程之間的中間層,讓進(jìn)程通過(guò)虛擬內(nèi)存來(lái)訪問物理內(nèi)存。引入了虛擬內(nèi)存后的操作系統(tǒng)如圖所示陆蟆。
用戶程序(進(jìn)程)只能使用虛擬的內(nèi)存地址來(lái)獲取數(shù)據(jù)雷厂,進(jìn)程會(huì)通過(guò)頁(yè)表中的虛擬內(nèi)存地址查看Memory Map,判斷當(dāng)前要訪問的虛擬內(nèi)存地址叠殷,是否已經(jīng)加載到了物理內(nèi)存改鲫。如果已經(jīng)在物理內(nèi)存,則取物理內(nèi)存數(shù)據(jù)林束,如果沒有對(duì)應(yīng)的物理內(nèi)存钩杰,則從磁盤加載數(shù)據(jù)到物理內(nèi)存,并把物理內(nèi)存地址和虛擬內(nèi)存地址更新到頁(yè)表诊县。
引入虛擬內(nèi)存后讲弄,每個(gè)進(jìn)程都有各自的虛擬內(nèi)存,內(nèi)存的并發(fā)訪問問題的粒度從多進(jìn)程級(jí)別依痊,可以降低到多線程級(jí)別避除。從程序的角度來(lái)看,它覺得自己獨(dú)享了一整塊內(nèi)存胸嘁,且不用考慮訪問沖突的問題瓶摆。系統(tǒng)會(huì)將虛擬地址翻譯成物理地址,從內(nèi)存上加載數(shù)據(jù)性宏。但如果僅僅把虛擬內(nèi)存直接理解為地址的映射關(guān)系群井,那就是過(guò)于低估虛擬內(nèi)存的作用了。
虛擬內(nèi)存的目的是為了解決以下幾件事:
(1)物理內(nèi)存無(wú)法被最大化利用毫胜。
(2)程序邏輯內(nèi)存空間使用獨(dú)立书斜。
(3)內(nèi)存不夠,繼續(xù)虛擬磁盤空間酵使。
2.2 讀時(shí)共享荐吉,寫時(shí)復(fù)制
其中針對(duì)(1)的最大化,虛擬內(nèi)存還實(shí)現(xiàn)了“讀時(shí)共享口渔,寫時(shí)復(fù)制
”的機(jī)制渴语,可以在物理層同一個(gè)字節(jié)的內(nèi)存地址被多個(gè)虛擬內(nèi)存空間映射搏存,表現(xiàn)方式下圖所示。
上圖所示如果一個(gè)進(jìn)程需要進(jìn)行寫操作迫筑,則這個(gè)內(nèi)存將會(huì)被復(fù)制一份且轨,成為當(dāng)前進(jìn)程的獨(dú)享內(nèi)存乍楚。如果是讀操作睡互,可能多個(gè)進(jìn)程訪問的物理空間是相同的空間玻淑。
如果一個(gè)內(nèi)存幾乎大量都是被讀取的,則可能會(huì)多個(gè)進(jìn)程共享同一塊物理內(nèi)存秘蛔,但是他們的各自虛擬內(nèi)存是不同的陨亡。當(dāng)然這個(gè)共享并不是永久的傍衡,當(dāng)其中有一個(gè)進(jìn)程對(duì)這個(gè)內(nèi)存發(fā)生寫深员,就會(huì)復(fù)制一份负蠕,執(zhí)行寫操作的進(jìn)程就會(huì)將虛擬內(nèi)存地址映射到新的物理內(nèi)存地址上。
2.3 虛擬內(nèi)存映射磁盤空間
對(duì)于第(3)點(diǎn)倦畅,是虛擬內(nèi)存為了最大化利用物理內(nèi)存遮糖,如果進(jìn)程使用的內(nèi)存足夠大,則導(dǎo)致物理內(nèi)存短暫的供不應(yīng)求叠赐,那么虛擬內(nèi)存也會(huì)“開疆拓土”從磁盤(硬盤)上虛擬出一定量的空間欲账,掛在虛擬地址上,而且這個(gè)動(dòng)作進(jìn)程本身是不知道的芭概,因?yàn)檫M(jìn)程只能夠看見自己的虛擬內(nèi)存空間赛不,如下圖所示。
綜上可見虛擬內(nèi)存的重要性罢洲,不僅提高了利用率而且整條內(nèi)存調(diào)度的鏈路完全是對(duì)用戶態(tài)物理內(nèi)存透明踢故,用戶可以安心的使用自身進(jìn)程獨(dú)立的虛擬內(nèi)存空間進(jìn)行開發(fā)。
3. 頁(yè)惹苗、頁(yè)表殿较、頁(yè)表?xiàng)l目
-
頁(yè)
頁(yè)是1次內(nèi)存讀取的大小,操作系統(tǒng)中用來(lái)描述內(nèi)存大小的一個(gè)單位名稱
桩蓉。一個(gè)頁(yè)的含義是大小為4K(1024*4=4096字節(jié)淋纲,可以配置,不同操作系統(tǒng)不一樣)的內(nèi)存空間院究。操作系統(tǒng)對(duì)虛擬內(nèi)存空間是按照這個(gè)單位來(lái)管理的洽瞬。 -
頁(yè)表
頁(yè)表實(shí)際上就是頁(yè)表?xiàng)l目(PTE)的集合,就是基于PTE的一個(gè)數(shù)組业汰,頁(yè)表的大小是以頁(yè)(4K)為單位的片任。
虛擬內(nèi)存的實(shí)現(xiàn)方式,大多數(shù)都是通過(guò)頁(yè)表來(lái)實(shí)現(xiàn)的蔬胯。操作系統(tǒng)虛擬內(nèi)存空間分成一頁(yè)一頁(yè)的來(lái)管理对供,每頁(yè)的大小為 4K(當(dāng)然這是可以配置的,不同操作系統(tǒng)不一樣)氛濒。4K 算是通過(guò)實(shí)踐折中出來(lái)的通用值产场,太小了會(huì)出現(xiàn)頻繁的置換,太大了又浪費(fèi)內(nèi)存舞竿。 -
頁(yè)表?xiàng)l目(PTE)
頁(yè)表?xiàng)l目(PTE)是頁(yè)表中的一個(gè)元素京景,PTE是真正起到虛擬內(nèi)存尋址作用的元素。PTE的內(nèi)部結(jié)構(gòu)如下圖所示骗奖。
PTE是由一個(gè)有效位和一個(gè)包含物理頁(yè)號(hào)或者磁盤地址組成确徙,有效位表示當(dāng)前虛擬頁(yè)是否已經(jīng)被緩存在主內(nèi)存中(或者CPU的高速緩存Cache中)醒串。
(1)有效位為1,表示虛擬頁(yè)已經(jīng)被緩存在內(nèi)存(或者CPU高速緩存TLB-Cache)中鄙皇。
(2)有效位為0芜赌,表示虛擬頁(yè)未被創(chuàng)建且沒有占用內(nèi)存(或者CPU高速緩存TLB-Cache),或者表示已經(jīng)創(chuàng)建虛擬頁(yè)但是并沒有存儲(chǔ)到內(nèi)存(或者CPU高速緩存TLB-Cache)中伴逸。
通過(guò)上述的標(biāo)識(shí)位缠沈,可以將虛擬頁(yè)集合分成三個(gè)子集,下表所示错蝴。
有效位 | 集合特征 |
---|---|
1 | 虛擬內(nèi)存已創(chuàng)建和分配頁(yè)洲愤,已緩存在物理內(nèi)存中。 |
0 | 虛擬內(nèi)存還未分配或創(chuàng)建顷锰。 |
0 | 虛擬內(nèi)存已創(chuàng)建和分配頁(yè)柬赐,但未緩存在物理內(nèi)存中。 |
4. CPU訪問內(nèi)存過(guò)程
當(dāng)某個(gè)進(jìn)程進(jìn)行一次內(nèi)存訪問指令請(qǐng)求官紫,將觸發(fā)上圖的內(nèi)存訪問肛宋,具體的訪問流程如下:
- 進(jìn)程將訪問內(nèi)存相關(guān)的指令請(qǐng)求發(fā)送給CPU,CPU接受到指令請(qǐng)求
- CPU找到數(shù)據(jù)的虛擬地址(可能存放在寄存器內(nèi)万矾,所以這一步就已經(jīng)包括寄存器的全部工作了悼吱。)
- 將虛擬地址(
Virtual Page Number
及offset
僅是其中一部分,我們這里只展示這兩部分的作用)送往內(nèi)存管理單元(MMU) - MMU先判斷TLB(Translation Look-aside Buffer)中是否緩存了該虛擬地址的物理地址良狈,如果命中后添,MMU直接獲取物理地址
- 如果TLB未命中,則將虛擬地址發(fā)送給Table Walk Unit
- Table Walk Unit根據(jù)虛擬地址的VPN獲取到一級(jí)頁(yè)表(頁(yè)目錄)薪丁,再?gòu)囊患?jí)頁(yè)表中獲取到二級(jí)頁(yè)表遇西,從二級(jí)頁(yè)表中獲取到對(duì)應(yīng)的物理內(nèi)存頁(yè)地址,結(jié)合虛擬地址中的物理內(nèi)存頁(yè)偏移量offset严嗜,拿到物理內(nèi)存頁(yè)中其中1項(xiàng)的物理地址
- 如果MMU未能查到物理地址粱檀,則會(huì)觸發(fā)缺頁(yè)異常;缺頁(yè)異常被捕獲后漫玄,操作系統(tǒng)會(huì)根據(jù)缺頁(yè)異常類型茄蚯,做出不同的處理。
- 如果MMU獲取到了物理地址睦优,則根據(jù)物理地址到Cache中查看是否已緩存了對(duì)應(yīng)的內(nèi)存數(shù)據(jù)渗常,如果緩存了則返回內(nèi)存數(shù)據(jù)
- 如果Cache未命中,則直接拿物理地址到主存中查看是否存在內(nèi)存數(shù)據(jù)汗盘,如果緩存了則返回內(nèi)存數(shù)據(jù)
5. 局部性
一個(gè)優(yōu)秀的程序通常具有良好的局部性皱碘,它們通常會(huì)重復(fù)使用已用過(guò)的數(shù)據(jù),或者使用已用過(guò)數(shù)據(jù)的鄰近數(shù)據(jù)隐孽,也就是說(shuō)癌椿,程序常常會(huì)使用集中在一起的局部數(shù)據(jù)健蕊。局部性分為:時(shí)間局部性和空間局部性。
- 空間局部性:一個(gè)內(nèi)存位置被引用過(guò)一次踢俄,在短時(shí)間內(nèi)缩功,其附近的內(nèi)存位置也將被引用。(內(nèi)存都是按頁(yè)讀取褪贵,讀取1個(gè)內(nèi)存位置后掂之,其所在頁(yè)的內(nèi)存數(shù)據(jù)會(huì)被緩存抗俄,所以再次讀取其附近的內(nèi)存位置效率更高)
- 時(shí)間局部性:被引用過(guò)一次的內(nèi)存位置脆丁,在短時(shí)間內(nèi)將被多次引用。(執(zhí)行效率越高的緩存动雹,容量越小槽卫。讀取1個(gè)內(nèi)存位置后,長(zhǎng)時(shí)間不再讀取此內(nèi)存位置胰蝠,會(huì)有新的內(nèi)存位置被緩存歼培,該內(nèi)存位置可能不再存在緩存中)
6. 棧和堆
Linux為每個(gè)進(jìn)程維護(hù)了一個(gè)單獨(dú)的虛擬地址空間,并且這個(gè)地址空間是連續(xù)的茸塞,進(jìn)程就可以很方便地訪問內(nèi)存躲庄,也就是我們常說(shuō)的虛擬內(nèi)存。虛擬內(nèi)存形式如下圖所示钾虐。
一個(gè)進(jìn)程的地址空間通常包括代碼段噪窘、數(shù)據(jù)段、堆效扫、棧等倔监,地址從低到高。代碼中使用的內(nèi)存地址都是虛擬內(nèi)存地址菌仁,而不是實(shí)際的物理內(nèi)存地址浩习。棧和堆只是虛擬內(nèi)存上2塊不同功能的內(nèi)存區(qū)域。在x64架構(gòu)中济丘,使用rsp寄存器指向棧頂谱秽;在x86架構(gòu)中,使用esp寄存器指向棧頂?shù)膬?nèi)存地址摹迷。一般可以簡(jiǎn)稱為sp疟赊。
- 棧
-
由編譯器自動(dòng)分配和釋放,速度快
棧中存儲(chǔ)著函數(shù)的入?yún)⒁约熬植孔兞坷嵯疲@些參數(shù)(如函數(shù)參數(shù)听绳、函數(shù)返回地址,局部變量异赫、臨時(shí)變量等)會(huì)隨著函數(shù)的創(chuàng)建而創(chuàng)建椅挣,函數(shù)的返回而銷毀头岔。(通過(guò) CPU push & release) -
棧的特性:后入先出LIFO
棧需要存儲(chǔ)函數(shù)中的局部變量和參數(shù),函數(shù)又是最后調(diào)用的最先銷毀鼠证,棧的后進(jìn)先出正好滿足這一點(diǎn)峡竣。 -
棧由高地址向低地址擴(kuò)展,棧內(nèi)是連續(xù)分配內(nèi)存的
如果給一個(gè)數(shù)組或?qū)ο蠓峙鋬?nèi)存量九,棧會(huì)選擇還沒分配的最小的內(nèi)存地址給數(shù)組适掰,在這個(gè)內(nèi)存塊中,數(shù)組中的元素從低地址到高地址依次分配(不要和棧的從高到低弄混了)荠列。所以數(shù)組中第一個(gè)元素的其實(shí)地址對(duì)應(yīng)于已分配棧的最低地址类浪。 -
棧只能獲取棧頂?shù)膬?nèi)存地址
棧是從高地址往低地址擴(kuò)展的,棧頂正好指向數(shù)組的起始地址肌似,即數(shù)組的指針费就。
- 棧楨
-
棧幀本質(zhì)上是一種棧
棧幀本質(zhì)上是一種棧。棧幀是指函數(shù)在被調(diào)用時(shí)川队,所擁有的一塊獨(dú)立的用于存放函數(shù)所使用的狀態(tài)和變量的椓ο福空間。 -
函數(shù)的每次進(jìn)入固额,都對(duì)應(yīng)1個(gè)棧楨
每個(gè)函數(shù)都對(duì)應(yīng)有一個(gè)棧幀眠蚂。同一個(gè)函數(shù)多次調(diào)用,每次可能會(huì)分配到不同的棧幀斗躏。整個(gè)棧的內(nèi)容在同一個(gè)時(shí)刻可以看作是由許多棧幀依序“堆疊”組成的逝慧。棧楨的結(jié)構(gòu)詳見下圖。
對(duì)于一個(gè)運(yùn)行中的函數(shù)瑟捣,其使用的棧幀區(qū)域被sp和bp寄存器限定(對(duì)于x86馋艺,sp等價(jià)esp,bp等價(jià)rsp迈套;對(duì)于x64捐祠,sp等價(jià)rsp,bp等價(jià)rbp)桑李。bp指向棧幀的底部踱蛀,sp指向棧幀的頂部。
在函數(shù)中使用的所有變量(本地變量贵白、實(shí)參)率拒,一般使用bp定位。設(shè)N為整型字節(jié)數(shù)禁荒,bp+2N是第一個(gè)實(shí)參的地址猬膨,bp-N是第一個(gè)本地變量的地址。
- 堆
- 自由分配呛伴,自己申請(qǐng)勃痴,自己釋放(否則發(fā)生內(nèi)存泄漏)谒所,速度較慢,更靈活
- 堆的特性:先入先出FIFO
- 堆的內(nèi)存地址是不連續(xù)的沛申,由低地址向高地址擴(kuò)展劣领,一般是鏈表結(jié)構(gòu)
由于棧都會(huì)隨著函數(shù)的創(chuàng)建而創(chuàng)建,函數(shù)的返回而銷毀铁材。所以我們大多時(shí)候談到的內(nèi)存管理尖淘,都是對(duì)堆內(nèi)存的管理。
TCMalloc
Golang的內(nèi)存管理是基于TCMalloc的核心思想來(lái)構(gòu)建的著觉。在了解Golang的內(nèi)存管理之前村生,一定要了解TCMalloc(Thread Cache Malloc)的內(nèi)存申請(qǐng)模式。隨著Go的迭代固惯,Go的內(nèi)存管理與TCMalloc不一致地方在不斷擴(kuò)大梆造,但其主要思想缴守、原理和概念都是和TCMalloc一致的葬毫,如果跳過(guò)TCMalloc直接去看Go的內(nèi)存管理,也許你會(huì)似懂非懂屡穗。本節(jié)將介紹TCMalloc的基礎(chǔ)理念和結(jié)構(gòu)贴捡。
在Linux操作系統(tǒng)中,其實(shí)有不少的內(nèi)存管理庫(kù)村砂,內(nèi)存管理庫(kù)的本質(zhì)都是在多線程編程下烂斋,追求更高內(nèi)存管理效率:更快的分配是主要目的。
通過(guò)引入虛擬內(nèi)存础废,使每個(gè)進(jìn)程擁有自己獨(dú)立的虛擬內(nèi)存汛骂,讓內(nèi)存的并發(fā)訪問問題的粒度從多進(jìn)程級(jí)別,降低到多線程級(jí)別评腺。然而同一進(jìn)程下的所有線程仍會(huì)共享相同的內(nèi)存空間帘瞭,它們申請(qǐng)內(nèi)存時(shí)需要加鎖,如果不加鎖就存在同一塊進(jìn)程內(nèi)存被2個(gè)線程同時(shí)訪問的問題蒿讥。
TCMalloc的做法是什么呢蝶念?為每個(gè)線程預(yù)分配一塊緩存,線程申請(qǐng)小內(nèi)存時(shí)芋绸,可以從緩存分配內(nèi)存媒殉,這樣有2個(gè)好處:
- 預(yù)分配緩存需要進(jìn)行1次系統(tǒng)調(diào)用,后續(xù)線程申請(qǐng)小內(nèi)存時(shí)直接從緩存分配摔敛,都是在用戶態(tài)執(zhí)行的廷蓉,沒有了系統(tǒng)調(diào)用,縮短了內(nèi)存總體的分配和釋放時(shí)間
- 多個(gè)線程同時(shí)申請(qǐng)小內(nèi)存時(shí)马昙,從各自的緩存分配桃犬,不再需要加鎖
1. 基本原理
結(jié)合上圖售貌,我們依次介紹下TCMalloc的幾個(gè)重要概念:
Page
TCMalloc執(zhí)行內(nèi)存管理的一種單位。操作系統(tǒng)執(zhí)行內(nèi)存管理以頁(yè)單位疫萤,TCMalloc里的Page大小與操作系統(tǒng)里的頁(yè)大小并不一定相等颂跨,而是整數(shù)倍關(guān)系〕度模《TCMalloc解密》里稱x64下Page大小是8KB恒削。-
Span
每個(gè)Span記錄了第一個(gè)起始Page的編號(hào)Start兰怠,和一共有多少個(gè)連續(xù)Page的數(shù)量Length梦鉴。
一個(gè)或多個(gè)連續(xù)的Page 組成一個(gè) Span,Span是TCMalloc中內(nèi)存管理的基本單位尾序,多個(gè)這樣的Span就用鏈表來(lái)管理钓丰。比如可以有1個(gè)Page大小的Span,也可以有2個(gè)Page大小的Span每币,Span比Page高一個(gè)層級(jí)携丁,是為了方便管理一定大小的內(nèi)存區(qū)域。 -
Size Class
Size Class是空間規(guī)格。TCMalloc會(huì)將這些小對(duì)象集合劃分成多個(gè)內(nèi)存刻度揭保,同屬于一個(gè)刻度類別下的內(nèi)存集合稱之為屬于一個(gè)Size Class肥橙。每個(gè)Size Class都對(duì)應(yīng)一個(gè)大小比如8字節(jié)、16字節(jié)秸侣、32字節(jié)等存筏。在申請(qǐng)小對(duì)象內(nèi)存的時(shí)候,TCMalloc會(huì)根據(jù)使用方申請(qǐng)的空間大小就近向上取最接近的一個(gè)Size Class的Span(由多個(gè)等空間的Page組成)內(nèi)存塊返回給使用方味榛。
如果將Size Class椭坚、Span、Page用一張圖來(lái)表示搏色,則具體的抽象關(guān)系如下圖所示
-
ThreadCache
ThreadCache是每個(gè)線程各自的Cache善茎,一個(gè)Cache包含多個(gè)空閑內(nèi)存塊鏈表,每個(gè)鏈表連接的都是大小相同的內(nèi)存塊继榆。也可以說(shuō)按內(nèi)存塊大小巾表,給內(nèi)存塊分了個(gè)類,這樣可以根據(jù)申請(qǐng)的內(nèi)存大小略吨,快速?gòu)暮线m的鏈表選擇空閑內(nèi)存塊集币。由于每個(gè)線程有自己的ThreadCache,所以ThreadCache訪問是無(wú)鎖的翠忠。
-
CentralCache
CentralCache是所有線程共享的緩存鞠苟,也是保存的空閑內(nèi)存塊鏈表,鏈表的數(shù)量與ThreadCache中鏈表數(shù)量相同,當(dāng)ThreadCache的內(nèi)存塊不足時(shí)当娱,可以從CentralCache獲取內(nèi)存塊吃既;當(dāng)ThreadCache內(nèi)存塊過(guò)多時(shí),可以放回CentralCache跨细。由于CentralCache是共享的鹦倚,所以它的訪問是要加鎖的。
-
PageHeap
PageHeap是對(duì)堆內(nèi)存的抽象冀惭,PageHeap存的是若干Span鏈表震叙。
如下圖所示,分別是1頁(yè)P(yáng)age的Span鏈表散休,2頁(yè)P(yáng)age的Span鏈表等媒楼,最后是large span set用來(lái)保存中、大對(duì)象戚丸。為了方便Span和Span之間的管理划址,Span集合是以雙向鏈表的形式構(gòu)建。
2. 分配流程
TCMalloc中對(duì)于對(duì)象類型的劃分:
- 小對(duì)象大惺聘妗:0~256KB
- 中對(duì)象大猩甙啤:257~1MB
- 大對(duì)象大小:>1MB
TCMalloc內(nèi)存分配流程:
-
小對(duì)象的分配流程
具體分配流程遗遵,詳見下圖
ThreadCache -> CentralCache -> HeapPage咱台,大部分時(shí)候络拌,ThreadCache緩存都是足夠的,不需要去訪問CentralCache和HeapPage回溺,無(wú)系統(tǒng)調(diào)用配合無(wú)鎖分配春贸,分配效率是非常高的。 -
中對(duì)象分配流程
中對(duì)象為大于256KB且小于等于1MB的內(nèi)存萍恕。直接在PageHeap中選擇適當(dāng)?shù)拇笮〖纯桑?28 Pages的Span所保存的最大內(nèi)存就是1MB。
具體分配流程车要,詳見下圖 -
大對(duì)象分配流程
對(duì)于超過(guò)128個(gè)Page(即1MB)的內(nèi)存分配則為大對(duì)象分配流程允粤。從PageHeap中的large span set選擇合適數(shù)量的頁(yè)面組成span,用來(lái)存儲(chǔ)數(shù)據(jù)。
具體分配流程类垫,詳見下圖
Go堆內(nèi)存管理
1. Go內(nèi)存模型層級(jí)結(jié)構(gòu)
Golang內(nèi)存管理模型與TCMalloc的設(shè)計(jì)極其相似司光。基本輪廓和概念也幾乎相同悉患,只是一些規(guī)則和流程存在差異残家。
2. Go內(nèi)存管理的基本概念
Go內(nèi)存管理的許多概念在TCMalloc中已經(jīng)有了,含義是相同的售躁,只是名字有一些變化跪削。
2.1 Page
與TCMalloc中的Page相同,x64架構(gòu)下1個(gè)Page的大小是8KB迂求。Page表示Golang內(nèi)存管理與虛擬內(nèi)存交互內(nèi)存的最小單元碾盐。操作系統(tǒng)虛擬內(nèi)存對(duì)于Golang來(lái)說(shuō),依然是劃分成等分的N個(gè)Page組成的一塊大內(nèi)存公共池揩局。
2.2 mspan
與TCMalloc中的Span一致毫玖。mspan概念依然延續(xù)TCMalloc中的Span概念,在Golang中將Span的名稱改為mspan,1個(gè)mspan為多個(gè)Page(go中為8KB的內(nèi)存大小)凌盯。1個(gè)mspan對(duì)應(yīng)1個(gè)或多個(gè)大小相同的object付枫,mspan主要用于分配對(duì)象的區(qū)塊,下圖簡(jiǎn)單說(shuō)明了Span的內(nèi)部結(jié)構(gòu)驰怎。mspan結(jié)構(gòu)體如下:
type mspan struct {
next *mspan // 在mspan鏈表中阐滩,指向后一個(gè)mspan
prev *mspan // 在mspan鏈表中,指向前一個(gè)mspan
list *mSpanList // 供debug使用
startAddr uintptr // mspan起始地址
npages uintptr // 當(dāng)前mspan對(duì)應(yīng)的page數(shù)
manualFreeList gclinkptr // mSpanManual狀態(tài)mspan中的可用對(duì)象鏈表
// freeindex是slot索引县忌,標(biāo)記下一次分配對(duì)象時(shí)應(yīng)該開始搜索的地址, 分配后freeindex會(huì)增加
// 每一次分配都從freeindex開始掃描allocBits掂榔,直到它遇到一個(gè)表示空閑對(duì)象的0
// 在freeindex之前的元素都是已分配的, 在freeindex之后的元素有可能已分配, 也有可能未分配
freeindex uintptr
nelems uintptr // 當(dāng)前span中object數(shù)量.
// allocCache是從freeindex位置開始的allocBits緩存
allocCache uint64
// allocBits用于標(biāo)記哪些元素是已分配的, 哪些元素是未分配的。
// 使用freeindex + allocBits可以在分配時(shí)跳過(guò)已分配的元素, 把對(duì)象設(shè)置在未分配的元素中.
allocBits *gcBits
// 用于在gc時(shí)標(biāo)記哪些對(duì)象存活, 每次gc以后allocBits都會(huì)與gcmarkBits保持一致
gcmarkBits *gcBits
// 清理代數(shù)症杏,每GC1次sweepgen會(huì)+2
// sweepgen=currrent sweepgen - 2:該span需要被清掃
// sweepgen=currrent sweepgen - 1:該span正在被清掃
// sweepgen=currrent sweepgen:該span已被清掃装获,帶使用
// sweepgen=currrent sweepgen + 1:該span在清掃開始前,仍然被緩存厉颤,需要被清掃
// sweepgen=currrent sweepgen + 3:該span已被清掃穴豫,仍然被緩存
sweepgen uint32
divMul uint32 // for divide by elemsize
allocCount uint16 // 已分配對(duì)象的數(shù)量
spanclass spanClass
state mSpanStateBox
needzero uint8 // 在分配前需要清零
elemsize uintptr // 對(duì)象大小
limit uintptr // span數(shù)據(jù)末尾
speciallock mutex // specials鏈表的鎖
specials *special // 根據(jù)object偏移量排序的special鏈表.
}
mspan的allocBits是一個(gè)bitmap,用于標(biāo)記哪些元素是已分配的, 哪些元素是未分配的逼友。通過(guò)使用allocBits已經(jīng)可以達(dá)到O(1)的分配速度精肃,但是go為了極限性能,對(duì)其做了一個(gè)緩存allocCache帜乞,allocCache是從freeindex開始的allocBits緩存司抱。
2.3 Size Class
Golang內(nèi)存管理針對(duì)衡量?jī)?nèi)存的概念又更加詳細(xì)了很多,這里面介紹一些基礎(chǔ)的有關(guān)內(nèi)存大小的名詞及算法挖函。
-
邏輯層從Golang內(nèi)存模型取內(nèi)存肉拓,實(shí)則是分配一個(gè)Object出去后频。為了更好的讓讀者理解,這里假設(shè)了幾個(gè)數(shù)據(jù)來(lái)標(biāo)識(shí)Object Size 和Span的關(guān)系暖途,如下圖所示卑惜。Object Class
是指協(xié)程應(yīng)用邏輯一次向Go內(nèi)存申請(qǐng)的對(duì)象Object大小状植。Object是Golang內(nèi)存管理模塊針對(duì)內(nèi)存管理更加細(xì)化的內(nèi)存管理單元浊竟。一個(gè)Span在初始化時(shí)會(huì)被分成多個(gè)Object。
比如Object Size是8B(8字節(jié))大小的Object津畸,所屬的Span大小是8KB(8192字節(jié))振定,那么這個(gè)Span就會(huì)被平均分割成1024(8192/8=1024)個(gè)Object。
Page是Golang內(nèi)存管理與操作系統(tǒng)交互時(shí),衡量?jī)?nèi)存容量的基本單元
Object是用來(lái)存儲(chǔ)一個(gè)變量數(shù)據(jù)的內(nèi)存空間驻售, 是Golang內(nèi)存管理為對(duì)象分配存儲(chǔ)內(nèi)存的基本單元
-
Size Class
是指Object大小的級(jí)別露久。比如Object Size在1Byte~8Byte之間的Object屬于Size Class 1級(jí)別,Object Size 在8B~16Byte之間的屬于Size Class 2級(jí)別欺栗。本質(zhì)上毫痕,golang的Size Class與TCMalloc中size class都是表示一塊內(nèi)存的所屬規(guī)格蜡塌。
go中共存在
_NumSizeClasses = 68
個(gè)Size Class(0~68),所以也對(duì)應(yīng)著68個(gè)Object Class
-
Span Class
是Golang內(nèi)存管理額外定義的規(guī)格屬性湿滓,也是針對(duì)Object大小來(lái)進(jìn)行劃分的。但是為了優(yōu)化GC Mark階段森渐,go內(nèi)部讓一個(gè)Size Class對(duì)應(yīng)2個(gè)Span Class类腮,其中一個(gè)Span為存放需要GC掃描的對(duì)象(包含指針的對(duì)象臊泰, scan span),另一個(gè)Span為存放不需要GC掃描的對(duì)象(不包含指針的對(duì)象蚜枢, noscan span)缸逃。
具體Span Class與Size Class的邏輯結(jié)構(gòu)關(guān)系如下圖所示嵌施。通過(guò)設(shè)置兩種span,讓GC掃描對(duì)象的時(shí)候祟偷,對(duì)于noscan的span可以不去查看bitmap區(qū)域來(lái)標(biāo)記子對(duì)象察滑。也就是說(shuō)進(jìn)行掃描的時(shí)候,直接判定該span中的對(duì)象不會(huì)存在引用對(duì)象修肠,不再進(jìn)行更深層的掃描,這樣可以大幅提升GC Mark的效率户盯。
其中Size Class和Span Class的對(duì)應(yīng)關(guān)系計(jì)算方式可以參考Golang源代碼,如下:
//usr/local/go/src/runtime/mheap.go
type spanClass uint8
//……(省略部分代碼)
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}
//……(省略部分代碼)
makeSpanClass()函數(shù)為通過(guò)Size Class來(lái)得到對(duì)應(yīng)的Span Class莽鸭,其中第二個(gè)形參noscan表示當(dāng)前對(duì)象是否需要GC掃描
吗伤,不難看出來(lái)Span Class 和Size Class的對(duì)應(yīng)關(guān)系公式如下表所示:
對(duì)象 | Size Class 與 Span Class對(duì)應(yīng)公式 |
---|---|
需要GC掃描是否存在引用對(duì)象 | Span Class = Size Class * 2 + 0 |
不需要GC掃描是否存在引用對(duì)象 | Span Class = Size Class * 2 + 1 |
Golang源碼里列舉了詳細(xì)的Size Class和Object大小、存放Object數(shù)量硫眨,以及每個(gè)Size Class對(duì)應(yīng)的Span內(nèi)存大小關(guān)系足淆,我們這里只展示部分:
//usr/local/go/src/runtime/sizeclasses.go
package runtime
// [class]: Size Class
// [bytes/obj]: Object Size,一次對(duì)外提供內(nèi)存Object的大小
// [bytes/span]: 當(dāng)前Object所對(duì)應(yīng)Span的內(nèi)存大小
// [objects]: 當(dāng)前Span一共有多少個(gè)Object
// [tail waste]: 當(dāng)前Span平均分N份Object后,會(huì)有多少內(nèi)存浪費(fèi)巧号。 ===> [bytes/span]%[bytes/obj]
// [max waste]: 當(dāng)前Size Class最大可能浪費(fèi)的空間所占百分比族奢。 ===> ((本級(jí)Object Size – (上級(jí)Object Size + 1))*本級(jí)Object數(shù)量) + [tail waste])/ 本級(jí)Span Size
// 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 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// ......
由以上源碼可見, 并沒有列舉Size Class為0的規(guī)格刻度內(nèi)存丹鸿。對(duì)于Span Class為0和1的越走,也就是對(duì)應(yīng)Size Class為0的規(guī)格刻度內(nèi)存,mcache實(shí)際上是沒有分配任何內(nèi)存的靠欢。因?yàn)镚olang內(nèi)存管理對(duì)內(nèi)存為0的數(shù)據(jù)申請(qǐng)做了特殊處理廊敌,如果申請(qǐng)的數(shù)據(jù)大小為0將直接返回一個(gè)固定內(nèi)存地址,不會(huì)走Golang內(nèi)存管理的正常邏輯门怪,詳見以下源碼
//usr/local/go/src/runtime/malloc.go
// Al Allocate an object of size bytes.
// Sm Small objects are allocated from the per-P cache's free lists.
// La Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ……(省略部分代碼)
if size == 0 {
return unsafe.Pointer(&zerobase)
}
//……(省略部分代碼)
}
上述代碼可以看見骡澈,如果申請(qǐng)的size為0,則直接return一個(gè)固定地址zerobase
掷空。所以在68種Size Class中肋殴,執(zhí)行newobject時(shí),會(huì)申請(qǐng)內(nèi)存的Size Class為67種拣帽。在Golang中如[0]int疼电、 struct{}所需要內(nèi)存大小均是0,這也是為什么很多開發(fā)者在通過(guò)Channel做同步時(shí)减拭,發(fā)送一個(gè)struct{}數(shù)據(jù)蔽豺,因?yàn)椴粫?huì)申請(qǐng)任何內(nèi)存,能夠適當(dāng)節(jié)省一部分內(nèi)存空間拧粪。
max waste為當(dāng)前Size Class最大可能浪費(fèi)的空間所占百分比計(jì)算方式,詳見下圖golang中[0]int修陡、 struct{}等,全部的0內(nèi)存對(duì)象分配可霎,返回的都是一個(gè)固定的地址魄鸦。
2.4 MCache
mcache與TCMalloc中的ThreadCache類似癣朗,但也有所不同拾因。
相同點(diǎn):都保存的是各種大小的Span,并按Span class分類旷余,小對(duì)象直接從此分配內(nèi)存绢记,起到了緩存的作用,并且可以無(wú)鎖訪問
不同點(diǎn):TCMalloc中是1個(gè)線程1個(gè)ThreadCache正卧,Go中是1個(gè)P擁有1個(gè)mcache蠢熄,兩者綁定關(guān)系的區(qū)別如下圖所示
如果將上圖的mcache展開,來(lái)看mcache的內(nèi)部構(gòu)造炉旷,則具體的結(jié)構(gòu)形式如下圖6所示
當(dāng)其中某個(gè)Span Class的MSpan已經(jīng)沒有可提供的Object時(shí)签孔,MCache則會(huì)向MCentral申請(qǐng)一個(gè)對(duì)應(yīng)的MSpan叉讥。mcache在初始化時(shí)是沒有任何mspan資源的,在使用過(guò)程中會(huì)動(dòng)態(tài)地申請(qǐng)饥追,不斷地去填充 alloc[numSpanClasses]*mspan图仓,通過(guò)雙向鏈表連接。
下面具體看一下mcache在源碼中的定義:
//go:notinheap
type mcache struct {
tiny uintptr //<16byte 申請(qǐng)小對(duì)象的起始地址
tinyoffset uintptr //從起始地址tiny開始的偏移量
local_tinyallocs uintptr //tiny對(duì)象分配的數(shù)量
alloc [numSpanClasses]*mspan // 分配的mspan list判耕,其中numSpanClasses=67*2透绩,索引是splanclassId
stackcache [_NumStackOrders]stackfreelist //棧緩存
local_largefree uintptr // 大對(duì)象釋放字節(jié)數(shù)
local_nlargefree uintptr // 釋放的大對(duì)象數(shù)量
local_nsmallfree [_NumSizeClasses]uintptr // 每種規(guī)格小對(duì)象釋放的個(gè)數(shù)
flushGen uint32 //掃描計(jì)數(shù)
}
MCache中每個(gè)Span Class都只會(huì)對(duì)應(yīng)一個(gè)MSpan對(duì)象,不同Span Class的MSpan的總體長(zhǎng)度不同壁熄,參考runtime/sizeclasses.go的標(biāo)準(zhǔn)規(guī)定劃分帚豪。比如對(duì)于Span Class為4的MSpan來(lái)說(shuō),存放內(nèi)存大小為1Page草丧,即8KB狸臣。每個(gè)對(duì)外提供的Object大小為16B,共存放512個(gè)Object昌执。其他Span Class的存放方式類似烛亦。
通過(guò)源碼可以看到MCache通過(guò)alloc[numSpanClasses]*mspan管理了很多不同規(guī)格不同類型的span,golang對(duì)于[16B,32KB]
的對(duì)象會(huì)使用這部分span進(jìn)行內(nèi)存分配懂拾,所有在這區(qū)間大小的對(duì)象都會(huì)從alloc這個(gè)數(shù)組里尋找煤禽。
var sizeclass uint8
//確定規(guī)格
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
} else {
sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
}
size = uintptr(class_to_size[sizeclass])
spc := makeSpanClass(sizeclass, noscan)
//alloc中查到
span := c.alloc[spc]
而對(duì)于更小的對(duì)象,我們叫它tiny對(duì)象岖赋,golang會(huì)通過(guò)tiny和tinyoffset組合尋找位置分配內(nèi)存空間檬果,這樣可以更好的節(jié)約空間,源碼如下:
off := c.tinyoffset
//根據(jù)不同大小內(nèi)存對(duì)齊
if size&7 == 0 {
off = round(off, 8)
} else if size&3 == 0 {
off = round(off, 4)
} else if size&1 == 0 {
off = round(off, 2)
}
if off+size <= maxTinySize && c.tiny != 0 {
// tiny+偏移量
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.local_tinyallocs++
mp.mallocing = 0
releasem(mp)
return x
}
// 空間不足從alloc重新申請(qǐng)空間用于tiny對(duì)象分配
span := c.alloc[tinySpanClass]
2.5 MCentral
MCentral與TCMalloc中的Central概念依然相似唐断。向MCentral申請(qǐng)Span是同樣是需要加鎖的选脊。
當(dāng)MCache的某個(gè)級(jí)別Span的內(nèi)存被分配光時(shí),它會(huì)向MCentral申請(qǐng)1個(gè)當(dāng)前級(jí)別的Span脸甘。
Goroutine恳啥、MCache、MCentral丹诀、MHeap互相交換的內(nèi)存單位是不同钝的,其中協(xié)程邏輯層與MCache的內(nèi)存交換單位是Object,MCache與MCentral铆遭、MCentral與MHeap的內(nèi)存交換單位是Span扁藕,MHeap與操作系統(tǒng)的內(nèi)存交換單位是Page。
MCentral與TCMalloc中的Central不同的是:CentralCache是每個(gè)級(jí)別的Span有1個(gè)鏈表疚脐,mcache是每個(gè)級(jí)別的Span有2個(gè)鏈表。如下圖所示邢疙。
MCentral屬于MHeap棍弄,MCentral是各個(gè)規(guī)格的mcentral集合望薄,實(shí)際上1個(gè)mcentral對(duì)應(yīng)1個(gè)Span Class,即Span Class個(gè)mcentral小內(nèi)存管理單元呼畸。對(duì)應(yīng)源碼為:
type mheap struct {
......
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
......
}
-
NonEmpty Span List
表示還有可用空間的Span鏈表痕支。鏈表中的所有Span都至少有1個(gè)空閑的Object空間。如果MCentral上游MCache退還Span蛮原,會(huì)將退還的Span加入到NonEmpty Span List鏈表中卧须。 -
Empty Span List
表示沒有可用空間的Span鏈表。該鏈表上的Span都不確定是否存在空閑的Object空間儒陨。如果MCentral提供給一個(gè)Span給到上游MCache花嘶,那么被提供的Span就會(huì)加入到Empty List鏈表中。
注意 在Golang 1.16版本之后蹦漠,MCentral中的NonEmpty Span List 和 Empty Span List
均由鏈表管理改成集合管理椭员,分別對(duì)應(yīng)Partial Span Set 和 Full Span Set。雖然存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)有變化笛园,但是基本的作用和職責(zé)沒有區(qū)別隘击。
下面是MCentral層級(jí)中其中一個(gè)Size Class級(jí)別的MCentral的定義Golang源代碼(V1.14版本):
//usr/local/go/src/runtime/mcentral.go , Go V1.14
// Central list of free objects of a given size.
// go:notinheap
type mcentral struct {
lock mutex //申請(qǐng)MCentral內(nèi)存分配時(shí)需要加的鎖
spanclass spanClass //當(dāng)前哪個(gè)Size Class級(jí)別的
// list of spans with a free object, ie a nonempty free list
// 還有可用空間的Span 鏈表
nonempty mSpanList
// list of spans with no free objects (or cached in an mcache)
// 沒有可用空間的Span鏈表,或者當(dāng)前鏈表里的Span已經(jīng)交給mcache
empty mSpanList
// nmalloc is the cumulative count of objects allocated from
// this mcentral, assuming all spans in mcaches are
// fully-allocated. Written atomically, read under STW.
// nmalloc是從該mcentral分配的對(duì)象的累積計(jì)數(shù)
// 假設(shè)mcaches中的所有跨度都已完全分配研铆。
// 以原子方式書寫埋同,在STW下閱讀。
nmalloc uint64
}
在GolangV1.16版本的相關(guān)MCentral結(jié)構(gòu)代碼如下:
//usr/local/go/src/runtime/mcentral.go , Go V1.16+
//…
type mcentral struct {
// mcentral對(duì)應(yīng)的spanClass
spanclass spanClass
partial [2]spanSet // 維護(hù)全部空閑的Span集合
full [2]spanSet // 維護(hù)存在非空閑的Span集合
}
//…
新版本的改進(jìn)是將List變成了兩個(gè)Set集合棵红,Partial集合與NonEmpty Span List責(zé)任類似凶赁,F(xiàn)ull集合與Empty Span List責(zé)任類似≌常可以看見Partial和Full都是一個(gè)[2]spanSet類型哟冬,也就每個(gè)Partial和Full都各有兩個(gè)spanSet集合,這是為了給GC垃圾回收來(lái)使用的忆绰,其中一個(gè)集合是已掃描的浩峡,另一個(gè)集合是未掃描的。
2.6 MHeap
Golang內(nèi)存管理的MHeap依然是繼承TCMalloc的PageHeap設(shè)計(jì)错敢。MHeap的上游是MCentral翰灾,MCentral中的Span不夠時(shí)會(huì)向MHeap申請(qǐng)。MHeap的下游是操作系統(tǒng)稚茅,MHeap的內(nèi)存不夠時(shí)會(huì)向操作系統(tǒng)的虛擬內(nèi)存空間申請(qǐng)纸淮。訪問MHeap獲取內(nèi)存依然是需要加鎖的。
MHeap是對(duì)內(nèi)存塊的管理對(duì)象亚享,是通過(guò)Page為內(nèi)存單元進(jìn)行管理咽块。那么用來(lái)詳細(xì)管理每一系列Page的結(jié)構(gòu)稱之為一個(gè)HeapArena,它們的邏輯層級(jí)關(guān)系如下圖所示欺税。
一個(gè)HeapArena占用內(nèi)存64MB侈沪,其中里面的內(nèi)存的是一個(gè)一個(gè)的mspan揭璃,當(dāng)然最小單元依然是Page,圖中沒有表示出mspan亭罪,因?yàn)槎鄠€(gè)連續(xù)的page就是一個(gè)mspan瘦馍。所有的HeapArena組成的集合是一個(gè)arenas [1]*[4M]*heapArena數(shù)組,運(yùn)行時(shí)使用arenas 管理所有的內(nèi)存应役。
mheap是Golang進(jìn)程全局唯一的情组,所以訪問依然加鎖。圖中又出現(xiàn)了mcentral箩祥,因?yàn)?strong>mcentral本也屬于mheap中的一部分院崇。只不過(guò)會(huì)優(yōu)先從MCentral獲取內(nèi)存,如果沒有mcentral會(huì)從Arenas中的某個(gè)heapArena獲取Page滥比。
heapArena結(jié)構(gòu)體如下:
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte // 用于標(biāo)記當(dāng)前這個(gè)HeapArena的內(nèi)存使用情況亚脆,1. 對(duì)應(yīng)地址中是否存在過(guò)對(duì)象、對(duì)象中哪些地址包含指針盲泛,2. 是否被GC標(biāo)記過(guò)濒持。主要用于GC
spans [pagesPerArena]*mspan // 存放heapArena中的span指針地址
pageInUse [pagesPerArena / 8]uint8 // 保存哪些spans處于mSpanInUse狀態(tài)
pageMarks [pagesPerArena / 8]uint8 // 保存哪些spans中包含被標(biāo)記的對(duì)象
pageSpecials [pagesPerArena / 8]uint8 // 保存哪些spans是特殊的
checkmarks *checkmarksMap // debug.gccheckmark state
zeroedBase uintptr //該arena第一頁(yè)的第一個(gè)字節(jié)地址
}
根據(jù)heapArena結(jié)構(gòu)體,我們可以了解到mheap內(nèi)存空間的邏輯視圖如下所示:
其中arena區(qū)域就是我們通常說(shuō)的heap, go從heap分配的內(nèi)存都在這個(gè)區(qū)域中寺滚。
其中spans區(qū)域用于表示arena區(qū)中的某一頁(yè)(Page)屬于哪個(gè)span柑营,spans區(qū)域中一個(gè)指針(8 byte)對(duì)應(yīng)了arena區(qū)域中的一頁(yè)(在go中一頁(yè)=8KB)。所以spans的大小是 512GB / 頁(yè)大小(8KB) * 指針大小(8 byte) = 512MB村视。spans區(qū)域和arenas區(qū)域的對(duì)應(yīng)關(guān)系如下圖所示:
其中每個(gè)HeapArean包含一個(gè)bitmap官套,其作用是用于標(biāo)記當(dāng)前這個(gè)HeapArena的內(nèi)存使用情況。
1個(gè)bitmap的邏輯結(jié)構(gòu)圖如下所示:
1個(gè)bitmap是8bit蚁孔,每一個(gè)指針大小的內(nèi)存都會(huì)有兩個(gè)bit分別表示是否應(yīng)該繼續(xù)掃描和是否包含指針奶赔,這樣1個(gè)byte就會(huì)對(duì)應(yīng)arena區(qū)域的四個(gè)指針大小的內(nèi)存。當(dāng)前HeapArena中的所有Page均會(huì)被bitmap所標(biāo)記杠氢,bitmap的主要作用是服務(wù)于GC垃圾回收模塊站刑。
bitmap中的byte和arena的對(duì)應(yīng)關(guān)系從末尾開始, 也就是隨著內(nèi)存分配會(huì)向兩邊擴(kuò)展
mheap結(jié)構(gòu)體如下:
type mheap struct {
lock mutex //必須在系統(tǒng)堆棧上獲得鼻百,否則當(dāng)G持有鎖時(shí)绞旅,堆棧增長(zhǎng),可能會(huì)自我死鎖
pages pageAlloc // page分配器數(shù)據(jù)結(jié)構(gòu)
sweepgen uint32 // 記錄span的sweep及cache狀態(tài)
sweepDrained uint32 // 所有的span都已被清掃温艇,或都正在被清掃
sweepers uint32 // 啟動(dòng)的swepper數(shù)量
allspans []*mspan // 曾經(jīng)創(chuàng)建的所有mspans地址的切片因悲,allspans的內(nèi)存是手動(dòng)管理的,可以隨著堆的增長(zhǎng)而重新分配和移動(dòng)勺爱。
// 一般來(lái)說(shuō)晃琳,allspans受到mheap_.lock的保護(hù),它可以防止并發(fā)訪問以及釋放后備存儲(chǔ)。
// 在STW期間的訪問可能不會(huì)持有鎖蝎土,但必須確保訪問周圍不能發(fā)生分配(因?yàn)檫@可能會(huì)釋放支持存儲(chǔ))视哑。
pagesInUse uint64 // pages所屬的spans處于狀態(tài)mSpanInUse; 原子式更新
pagesSwept uint64 // 本周期內(nèi)被清掃的pages數(shù); 原子式更新
pagesSweptBasis uint64 // 被用作Proportional sweep模式原點(diǎn)的pagesSwept; 原子式更新
sweepHeapLiveBasis uint64 // gcController.heapLive的值,作為掃描率的原點(diǎn)誊涯;帶鎖寫入,不帶鎖讀取蒜撮。
sweepPagesPerByte float64 // Proportional sweep比例; 寫時(shí)有鎖暴构,讀時(shí)無(wú)鎖
// TODO(austin): pagesInUse should be a uintptr, but the 386 compiler can't 8-byte align fields.
scavengeGoal uint64 // 維持的總的保留堆內(nèi)存量(運(yùn)行時(shí)試圖通過(guò)向操作系統(tǒng)返回內(nèi)存來(lái)維持該內(nèi)存量,該內(nèi)存量由heapRetained衡量)段磨。
reclaimIndex uint64 // 下一個(gè)要回收的page在allArenas中的索引
reclaimCredit uintptr
// arenas是*heapArena的map. 它指向整個(gè)可用的虛擬地址空間的每一個(gè)arena幀的堆的元數(shù)據(jù)取逾。
// 這是一個(gè)兩級(jí)映射,由一個(gè)L1映射和可能的許多L2映射組成苹支。當(dāng)有大量的arena時(shí)砾隅,這可以節(jié)省空間
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
heapArenaAlloc linearAlloc // 用于分配heapArena對(duì)象的預(yù)留空間。這只在32位上使用债蜜,我們預(yù)先保留這個(gè)空間以避免與堆本身交錯(cuò)晴埂。
arenaHints *arenaHint // arenaHints是一個(gè)地址列表,用于標(biāo)記哪里的heap arenas需要擴(kuò)容
arena linearAlloc // 是一個(gè)預(yù)先保留的空間寻定,用于分配heap arenas儒洛。只用在32位操作系統(tǒng)
allArenas []arenaIdx // 所有arena序號(hào)集合,可以根據(jù)arenaIdx算出對(duì)應(yīng)arenas中的那一個(gè)heapArena
sweepArenas []arenaIdx // sweepArenas是在掃描周期開始時(shí)對(duì)所有Arenas的快照狼速,通過(guò)禁用搶占可以安全讀取
markArenas []arenaIdx // markArenas是在標(biāo)記周期開始時(shí)對(duì)所有Arenas的快照琅锻,由于allArenas只可向后追加,并且標(biāo)記不會(huì)修改該切片內(nèi)容向胡,所以可以安全讀取
//curArena是堆當(dāng)前正在擴(kuò)容的區(qū)域恼蓬,curArena總是與physPageSize對(duì)齊
curArena struct {
base, end uintptr
}
// central 是存放small size classes的列表
central [numSpanClasses]struct {
mcentral mcentral
// pad確保mcentrals間隔CacheLinePadSize字節(jié),以便每個(gè)mcentral.lock得到它自己的緩存行
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
spanalloc fixalloc // allocator for span*
cachealloc fixalloc // allocator for mcache*
specialfinalizeralloc fixalloc // allocator for specialfinalizer*
specialprofilealloc fixalloc // allocator for specialprofile*
specialReachableAlloc fixalloc // allocator for specialReachable
speciallock mutex // lock for special record allocators.
arenaHintAlloc fixalloc // allocator for arenaHints
unused *specialfinalizer // never set, just here to force the specialfinalizer type into DWARF
}
arenaHint結(jié)構(gòu)體為:
type arenaHint struct {
addr uintptr // 為指向的對(duì)應(yīng)heapArena首地址僵芹。
down bool // 為當(dāng)前的heapArena是否可以擴(kuò)容处硬。
next *arenaHint // 指向下一個(gè)heapArena所對(duì)應(yīng)的ArenaHint首地址。
}
3. 內(nèi)存分配規(guī)則
介紹完內(nèi)存管理基本概念淮捆,我們?cè)賮?lái)總結(jié)一下內(nèi)存分配規(guī)則郁油,流程圖如下:
3.1 Tiny對(duì)象分配流程
- 判斷對(duì)象大小是否小于maxSmallSize=32KB,如果小于32KB則進(jìn)入Tiny對(duì)象或小對(duì)象申請(qǐng)流程攀痊,否則進(jìn)入大對(duì)象申請(qǐng)流程桐腌。
- 判斷對(duì)象大小是否小于maxTinySize=16B并且對(duì)象中是否包含指針,如果大于16B或包含指針苟径,則進(jìn)入小對(duì)象申請(qǐng)流程案站,否則進(jìn)入Tiny對(duì)象申請(qǐng)流程
- Tiny對(duì)象申請(qǐng)流程后,會(huì)先獲取mcache目前的tinyoffset棘街,再根據(jù)申請(qǐng)tiny對(duì)象的大小及mcache.tinyoffset值蟆盐,進(jìn)行內(nèi)存對(duì)齊承边,計(jì)算出滿足內(nèi)存對(duì)齊后的對(duì)象插入位置offset
- 如果從插入位置offset插入對(duì)象后,不超出16B石挂,并且存在待分配的tiny空間博助,則將對(duì)象填充到該tiny空間,并將地址返回給M痹愚,結(jié)束內(nèi)存申請(qǐng)
- 如果當(dāng)前的tiny空間不足富岳,則通過(guò)nextFreeFast(span)查找span中一個(gè)可用對(duì)象地址,存在則返回地址拯腮,并結(jié)束內(nèi)存申請(qǐng)
- 如果span中不存在一個(gè)可用對(duì)象窖式,則調(diào)用mcache.nextFree(tinySpanClass)從mcentral申請(qǐng)1個(gè)相同規(guī)格的msapn。申請(qǐng)成功則結(jié)束流程
3.2 小對(duì)象分配流程
- 進(jìn)入小對(duì)象申請(qǐng)流程后动壤,通過(guò)mcache.alloc(spc)獲取1個(gè)指定規(guī)格的mspan
- 通過(guò)nextFreeFast(span)查找span中一個(gè)可用對(duì)象地址萝喘,存在則返回地址給協(xié)程邏輯層P,P得到內(nèi)存空間琼懊,流程結(jié)束
- 如果不存在可用對(duì)象阁簸,則通過(guò)mcache.nextFree(tinySpanClass)中mcache.refill(spc)從mcentral申請(qǐng)1個(gè)相同規(guī)格的msapn
4.mcache.refill(spc)中,會(huì)首先嘗試通過(guò)mcentral的noempty list獲取mspan肩碟,獲取不到則在嘗試通過(guò)mcentral的empty list獲取mspan(1.16之后强窖,通過(guò)mcentral.cacheSpan()從partial set獲取mspan,獲取不到則從full set獲取可回收的mspan)削祈。mcache成功獲取mcentral返回的mspan后翅溺,返回可用對(duì)象地址,結(jié)束申請(qǐng)流程 - mcache中empty List(1.16之后髓抑,full set)也沒有可回收的mspan咙崎,則會(huì)調(diào)用mcache.grow()函數(shù),從mheap中申請(qǐng)內(nèi)存
- mheap收到內(nèi)存請(qǐng)求從其中一個(gè)heapArena從取出一部分pages返回給mcentral吨拍;當(dāng)mheap沒有足夠的內(nèi)存時(shí)褪猛,mheap會(huì)向操作系統(tǒng)申請(qǐng)內(nèi)存,將申請(qǐng)的內(nèi)存也保存到heapArena中的mspan中羹饰。mcentral將從mheap獲取的由Pages組成的mspan添加到對(duì)應(yīng)的span class鏈表或集合中
- 最后協(xié)程業(yè)務(wù)邏輯層得到該對(duì)象申請(qǐng)到的內(nèi)存伊滋,流程結(jié)束
3.3 大對(duì)象分配流程
- 進(jìn)入大對(duì)象分配流程后,會(huì)調(diào)用mcache.allocLarge()方法申請(qǐng)大對(duì)象
- mcache.allocLarge()中主要的mspan申請(qǐng)鏈路為:mheap.alloc -> mheap.allocSpan队秩,mheap.allocSpan為申請(qǐng)mspan的核心方法笑旺。mheap.allocSpan會(huì)首先判斷申請(qǐng)的page數(shù)是否小于P.pageCache的最大page數(shù),如果P.pageCache滿足需要馍资,則會(huì)從P.mspancache獲取mspan地址給P筒主,流程結(jié)束
- P.pageCache不足,則對(duì)mheap加鎖,從mheap.pageAlloc這種Radix tree(基數(shù)樹)數(shù)據(jù)結(jié)構(gòu)中查找可用的page乌妙,協(xié)程邏輯層P得到內(nèi)存使兔,流程結(jié)束
- mheap.pageAlloc中查找不存在可用的page,則調(diào)用mheap.grow()向操作系統(tǒng)申請(qǐng)內(nèi)存藤韵。申請(qǐng)成功后虐沥,再次從mheap.pageAlloc中查找可以page,P得到內(nèi)存后荠察,流程結(jié)束
References:
https://zhuanlan.zhihu.com/p/76802887
https://zhuanlan.zhihu.com/p/404813126
https://studygolang.com/articles/22500?fr=sidebar
https://www.yuque.com/aceld/golang/qzyivn
https://u.geekbang.org/lesson/267
https://u.geekbang.org/lesson/279
https://baike.baidu.com/item/%E5%86%85%E5%AD%98/103614
https://www.bilibili.com/video/BV1KD4y1U7Rr
https://www.zhihu.com/question/25142664
https://goalong.github.io/2019/09/20/%E8%BF%9B%E7%A8%8B%E4%B8%AD%E7%9A%84%E5%A0%86%E5%92%8C%E6%A0%88
https://blog.csdn.net/sandonz/article/details/117742864
https://ctfbook.ph0en1x.com/reverse/zhan-3001-zhan-zheng-yu-han-shu-diao-yong
https://www.cnblogs.com/jiujuan/p/13869547.html
https://note.youdao.com/noteshare?id=f843698db192fbbc887252f64d2214bd
https://zhuanlan.zhihu.com/p/266496735
https://zhuanlan.zhihu.com/p/53581298
https://www.cnblogs.com/zkweb/p/7880099.html