golang筆記——Go堆內(nèi)存管理

前言

本文主要記錄個(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)伟墙。

    1. DRAM:DRAM結(jié)構(gòu)較簡(jiǎn)單且集成度高翘鸭,通常用于制造內(nèi)存條中的存儲(chǔ)芯片。
    2. 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ǔ)器層次結(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)存訪問肛宋,具體的訪問流程如下:

  1. 進(jìn)程將訪問內(nèi)存相關(guān)的指令請(qǐng)求發(fā)送給CPU,CPU接受到指令請(qǐng)求
  2. CPU找到數(shù)據(jù)的虛擬地址(可能存放在寄存器內(nèi)万矾,所以這一步就已經(jīng)包括寄存器的全部工作了悼吱。)
  3. 將虛擬地址(Virtual Page Numberoffset僅是其中一部分,我們這里只展示這兩部分的作用)送往內(nèi)存管理單元(MMU)
  4. MMU先判斷TLB(Translation Look-aside Buffer)中是否緩存了該虛擬地址的物理地址良狈,如果命中后添,MMU直接獲取物理地址
  5. 如果TLB未命中,則將虛擬地址發(fā)送給Table Walk Unit
  6. 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)的物理地址
  7. 如果MMU未能查到物理地址粱檀,則會(huì)觸發(fā)缺頁(yè)異常;缺頁(yè)異常被捕獲后漫玄,操作系統(tǒng)會(huì)根據(jù)缺頁(yè)異常類型茄蚯,做出不同的處理。
  8. 如果MMU獲取到了物理地址睦优,則根據(jù)物理地址到Cache中查看是否已緩存了對(duì)應(yīng)的內(nèi)存數(shù)據(jù)渗常,如果緩存了則返回內(nèi)存數(shù)據(jù)
  9. 如果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)存形式如下圖所示钾虐。

Linux進(jìn)程的虛擬內(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疟赊。

  1. 由編譯器自動(dòng)分配和釋放,速度快
    棧中存儲(chǔ)著函數(shù)的入?yún)⒁约熬植孔兞坷嵯疲@些參數(shù)(如函數(shù)參數(shù)听绳、函數(shù)返回地址,局部變量异赫、臨時(shí)變量等)會(huì)隨著函數(shù)的創(chuàng)建而創(chuàng)建椅挣,函數(shù)的返回而銷毀头岔。(通過(guò) CPU push & release)
  2. 棧的特性:后入先出LIFO
    棧需要存儲(chǔ)函數(shù)中的局部變量和參數(shù),函數(shù)又是最后調(diào)用的最先銷毀鼠证,棧的后進(jìn)先出正好滿足這一點(diǎn)峡竣。
  3. 棧由高地址向低地址擴(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)于已分配棧的最低地址类浪。
  4. 棧只能獲取棧頂?shù)膬?nèi)存地址
    棧是從高地址往低地址擴(kuò)展的,棧頂正好指向數(shù)組的起始地址肌似,即數(shù)組的指針费就。
  • 棧楨
  1. 棧幀本質(zhì)上是一種棧
    棧幀本質(zhì)上是一種棧。棧幀是指函數(shù)在被調(diào)用時(shí)川队,所擁有的一塊獨(dú)立的用于存放函數(shù)所使用的狀態(tài)和變量的椓ο福空間。
  2. 函數(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è)本地變量的地址。
  1. 自由分配呛伴,自己申請(qǐng)勃痴,自己釋放(否則發(fā)生內(nèi)存泄漏)谒所,速度較慢,更靈活
  2. 堆的特性:先入先出FIFO
  3. 堆的內(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è)好處:

  1. 預(yù)分配緩存需要進(jìn)行1次系統(tǒng)調(diào)用,后續(xù)線程申請(qǐng)小內(nèi)存時(shí)直接從緩存分配摔敛,都是在用戶態(tài)執(zhí)行的廷蓉,沒有了系統(tǒng)調(diào)用,縮短了內(nèi)存總體的分配和釋放時(shí)間
  2. 多個(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è)或多個(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ū)域。

    每個(gè)Span記錄了第一個(gè)起始Page的編號(hào)Start兰怠,和一共有多少個(gè)連續(xù)Page的數(shù)量Length梦鉴。
  • 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)建

    當(dāng)CentralCache的內(nèi)存不足時(shí)限府,會(huì)從PageHeap獲取空閑的內(nèi)存Span夺颤,然后把1個(gè)Span拆成若干內(nèi)存塊,添加到對(duì)應(yīng)大小的鏈表中并分配內(nèi)存谣殊;當(dāng)CentralCache的內(nèi)存過(guò)多時(shí)拂共,會(huì)把空閑的內(nèi)存塊放回PageHeap中。毫無(wú)疑問姻几,PageHeap也是要加鎖的。

2. 分配流程

TCMalloc中對(duì)于對(duì)象類型的劃分:

  1. 小對(duì)象大惺聘妗:0~256KB
  2. 中對(duì)象大猩甙啤:257~1MB
  3. 大對(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)存大小的名詞及算法挖函。

  1. 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。

    邏輯層從Golang內(nèi)存模型取內(nèi)存肉拓,實(shí)則是分配一個(gè)Object出去后频。為了更好的讓讀者理解,這里假設(shè)了幾個(gè)數(shù)據(jù)來(lái)標(biāo)識(shí)Object Size 和Span的關(guān)系暖途,如下圖所示卑惜。

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)存的基本單元

  1. 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

  1. 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)缸逃。

通過(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的效率户盯。

具體Span Class與Size Class的邏輯結(jié)構(gòu)關(guān)系如下圖所示嵌施。

其中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)存空間拧粪。

golang中[0]int修陡、 struct{}等,全部的0內(nèi)存對(duì)象分配可霎,返回的都是一個(gè)固定的地址魄鸦。

max waste為當(dāng)前Size Class最大可能浪費(fèi)的空間所占百分比計(jì)算方式,詳見下圖
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
    }
    ......
}
  1. NonEmpty Span List
    表示還有可用空間的Span鏈表痕支。鏈表中的所有Span都至少有1個(gè)空閑的Object空間。如果MCentral上游MCache退還Span蛮原,會(huì)將退還的Span加入到NonEmpty Span List鏈表中卧须。
  2. 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里面相關(guān)的數(shù)據(jù)結(jié)構(gòu)和指針依賴關(guān)系,可以參考下圖:

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ì)象分配流程
  1. 判斷對(duì)象大小是否小于maxSmallSize=32KB,如果小于32KB則進(jìn)入Tiny對(duì)象或小對(duì)象申請(qǐng)流程攀痊,否則進(jìn)入大對(duì)象申請(qǐng)流程桐腌。
  2. 判斷對(duì)象大小是否小于maxTinySize=16B并且對(duì)象中是否包含指針,如果大于16B或包含指針苟径,則進(jìn)入小對(duì)象申請(qǐng)流程案站,否則進(jìn)入Tiny對(duì)象申請(qǐng)流程
  3. 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
  4. 如果從插入位置offset插入對(duì)象后,不超出16B石挂,并且存在待分配的tiny空間博助,則將對(duì)象填充到該tiny空間,并將地址返回給M痹愚,結(jié)束內(nèi)存申請(qǐng)
  5. 如果當(dāng)前的tiny空間不足富岳,則通過(guò)nextFreeFast(span)查找span中一個(gè)可用對(duì)象地址,存在則返回地址拯腮,并結(jié)束內(nèi)存申請(qǐng)
  6. 如果span中不存在一個(gè)可用對(duì)象窖式,則調(diào)用mcache.nextFree(tinySpanClass)從mcentral申請(qǐng)1個(gè)相同規(guī)格的msapn。申請(qǐng)成功則結(jié)束流程
3.2 小對(duì)象分配流程
  1. 進(jìn)入小對(duì)象申請(qǐng)流程后动壤,通過(guò)mcache.alloc(spc)獲取1個(gè)指定規(guī)格的mspan
  2. 通過(guò)nextFreeFast(span)查找span中一個(gè)可用對(duì)象地址萝喘,存在則返回地址給協(xié)程邏輯層P,P得到內(nèi)存空間琼懊,流程結(jié)束
  3. 如果不存在可用對(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)流程
  4. mcache中empty List(1.16之后髓抑,full set)也沒有可回收的mspan咙崎,則會(huì)調(diào)用mcache.grow()函數(shù),從mheap中申請(qǐng)內(nèi)存
  5. 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鏈表或集合中
  6. 最后協(xié)程業(yè)務(wù)邏輯層得到該對(duì)象申請(qǐng)到的內(nèi)存伊滋,流程結(jié)束
3.3 大對(duì)象分配流程
  1. 進(jìn)入大對(duì)象分配流程后,會(huì)調(diào)用mcache.allocLarge()方法申請(qǐng)大對(duì)象
  2. 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é)束
  3. P.pageCache不足,則對(duì)mheap加鎖,從mheap.pageAlloc這種Radix tree(基數(shù)樹)數(shù)據(jù)結(jié)構(gòu)中查找可用的page乌妙,協(xié)程邏輯層P得到內(nèi)存使兔,流程結(jié)束
  4. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末置蜀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子悉盆,更是在濱河造成了極大的恐慌,老刑警劉巖馋吗,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焕盟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宏粤,警方通過(guò)查閱死者的電腦和手機(jī)脚翘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)绍哎,“玉大人来农,你說(shuō)我怎么就攤上這事〕缪撸” “怎么了沃于?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)海诲。 經(jīng)常有香客問我繁莹,道長(zhǎng),這世上最難降的妖魔是什么特幔? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任咨演,我火速辦了婚禮,結(jié)果婚禮上蚯斯,老公的妹妹穿的比我還像新娘薄风。我一直安慰自己,他們只是感情好拍嵌,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布遭赂。 她就那樣靜靜地躺著,像睡著了一般撰茎。 火紅的嫁衣襯著肌膚如雪嵌牺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音逆粹,去河邊找鬼募疮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛僻弹,可吹牛的內(nèi)容都是我干的阿浓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼蹋绽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼芭毙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起卸耘,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤退敦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蚣抗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侈百,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年翰铡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钝域。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锭魔,死狀恐怖例证,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迷捧,我是刑警寧澤织咧,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站党涕,受9級(jí)特大地震影響烦感,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜膛堤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一手趣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肥荔,春花似錦绿渣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至誉帅,卻和暖如春淀散,著一層夾襖步出監(jiān)牢的瞬間右莱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工档插, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留慢蜓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓郭膛,卻偏偏與公主長(zhǎng)得像晨抡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子则剃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 這篇文章可以看作是內(nèi)存管理這篇長(zhǎng)文的學(xué)習(xí)總結(jié)吧耘柱,原文基于源碼剖析了整個(gè)go的內(nèi)存管理,非常詳盡棍现。 程序中的數(shù)據(jù)和變...
    彥幀閱讀 1,082評(píng)論 0 0
  • go的內(nèi)存分配 Go在程序啟動(dòng)的時(shí)候调煎,會(huì)先向操作系統(tǒng)申請(qǐng)一塊內(nèi)存(注意這時(shí)還只是一段虛擬的地址空間,并不會(huì)真正地分...
    GGBond_8488閱讀 790評(píng)論 0 1
  • 一己肮、tcmalloc介紹<參考資源> go的內(nèi)存管理和tcmalloc(thread-caching malloc...
    簾外五更風(fēng)閱讀 454評(píng)論 0 0
  • 前 言 從實(shí)踐中看汛蝙,Golang(以下簡(jiǎn)稱Go)應(yīng)用程序比Java占用更少的內(nèi)存,這與它們的運(yùn)行時(shí)環(huán)境有關(guān)朴肺,其運(yùn)行...
    安靜的少女閱讀 1,622評(píng)論 0 5
  • 介紹 了解操作系統(tǒng)對(duì)內(nèi)存的管理機(jī)制后,現(xiàn)在可以去看下 Go 語(yǔ)言是如何利用底層的這些特性來(lái)優(yōu)化內(nèi)存的坚洽。Go 的內(nèi)存...
    達(dá)菲格閱讀 8,760評(píng)論 2 47