Go 語言內(nèi)存管理(一):系統(tǒng)內(nèi)存管理

介紹

要搞明白 Go 語言的內(nèi)存管理放航,就必須先理解操作系統(tǒng)以及機(jī)器硬件是如何管理內(nèi)存的。因?yàn)?Go 語言的內(nèi)部機(jī)制是建立在這個(gè)基礎(chǔ)之上的圆裕,它的設(shè)計(jì)三椿,本質(zhì)上就是盡可能的會(huì)發(fā)揮操作系統(tǒng)層面的優(yōu)勢,而避開導(dǎo)致低效情況葫辐。

操作系統(tǒng)內(nèi)存管理

其實(shí)現(xiàn)在計(jì)算機(jī)內(nèi)存管理的方式都是一步步演變來的,最開始是非常簡單的伴郁,后來為了滿足各種需求而增加了各種各樣的機(jī)制耿战,越來越復(fù)雜。這里我們只介紹和開發(fā)者息息相關(guān)的幾個(gè)機(jī)制焊傅。

最原始的方式

我們可以把內(nèi)存看成一個(gè)數(shù)組剂陡,每個(gè)數(shù)組元素的大小是 1B狈涮,也就是 8 位(bit)。CPU 通過內(nèi)存地址來獲取內(nèi)存中的數(shù)據(jù)鸭栖,內(nèi)存地址可以看做成數(shù)組的游標(biāo)(index)歌馍。

CPU 在執(zhí)行指令的時(shí)候,就是通過內(nèi)存地址界睁,將物理內(nèi)存上的數(shù)據(jù)載入到寄存器剩辟,然后執(zhí)行機(jī)器指令芥映。但隨著發(fā)展,出現(xiàn)了多任務(wù)的需求晓锻,也就是希望多個(gè)任務(wù)能同時(shí)在系統(tǒng)上運(yùn)行。這就出現(xiàn)了一些問題:

  1. 內(nèi)存訪問沖突:程序很容易出現(xiàn) bug飞几,就是 2 或更多的程序使用了同一塊內(nèi)存空間砚哆,導(dǎo)致數(shù)據(jù)讀寫錯(cuò)亂,程序崩潰屑墨。更有一些黑客利用這個(gè)缺陷來制作病毒躁锁。
  2. 內(nèi)存不夠用:因?yàn)槊總€(gè)程序都需要自己單獨(dú)使用的一塊內(nèi)存,內(nèi)存的大小就成了任務(wù)數(shù)量的瓶頸卵史。
  3. 程序開發(fā)成本高:你的程序要使用多少內(nèi)存战转,內(nèi)存地址是多少,這些都不能搞錯(cuò)程腹,對(duì)于人來說匣吊,開發(fā)正確的程序很費(fèi)腦子。

舉個(gè)例子寸潦,假設(shè)有一個(gè)程序色鸳,當(dāng)代碼運(yùn)行到某處時(shí),需要使用 100M 內(nèi)存见转,其他時(shí)候 1M 內(nèi)存就夠命雀;為了避免和其他程序沖突,程序初始化時(shí)斩箫,就必須申請(qǐng)獨(dú)立 100M 內(nèi)存以保證正常運(yùn)行吏砂,這就是一種很大的浪費(fèi),因?yàn)檫@ 100M 它大多數(shù)時(shí)候用不上乘客,其他程序還不能用狐血。

虛擬內(nèi)存

虛擬內(nèi)存的出現(xiàn),很好的為了解決上述的一些列問題易核。用戶程序只能使用虛擬的內(nèi)存地址來獲取數(shù)據(jù)匈织,系統(tǒng)會(huì)將這個(gè)虛擬地址翻譯成實(shí)際的物理地址。

所有程序統(tǒng)一使用一套連續(xù)虛擬地址,比如 0x0000 ~ 0xffff缀匕。從程序的角度來看纳决,它覺得自己獨(dú)享了一整塊內(nèi)存。不用考慮訪問沖突的問題乡小。系統(tǒng)會(huì)將虛擬地址翻譯成物理地址阔加,從內(nèi)存上加載數(shù)據(jù)。

對(duì)于內(nèi)存不夠用的問題满钟,虛擬內(nèi)存本質(zhì)上是將磁盤當(dāng)成最終存儲(chǔ)胜榔,而主存作為了一個(gè) cache。程序可以從虛擬內(nèi)存上申請(qǐng)很大的空間使用零远,比如 1G苗分;但操作系統(tǒng)不會(huì)真的在物理內(nèi)存上開辟 1G 的空間,它只是開辟了很小一塊牵辣,比如 1M 給程序使用摔癣。
這樣程序在訪問內(nèi)存時(shí),操作系統(tǒng)看訪問的地址是否能轉(zhuǎn)換成物理內(nèi)存地址纬向。能則正常訪問择浊,不能則再開辟。這使得內(nèi)存得到了更高效的利用逾条。

如下圖所示琢岩,每個(gè)進(jìn)程所使用的虛擬地址空間都是一樣的,但他們的虛擬地址會(huì)被映射到主存上的不同區(qū)域师脂,甚至映射到磁盤上(當(dāng)內(nèi)存不夠用時(shí))担孔。

虛擬地址

其實(shí)本質(zhì)上很簡單,就是操作系統(tǒng)將程序常用的數(shù)據(jù)放到內(nèi)存里加速訪問吃警,不常用的數(shù)據(jù)放在磁盤上糕篇。這一切對(duì)用戶程序來說完全是透明的,用戶程序可以假裝所有數(shù)據(jù)都在內(nèi)存里酌心,然后通過虛擬內(nèi)存地址去訪問數(shù)據(jù)拌消。在這背后,操作系統(tǒng)會(huì)自動(dòng)將數(shù)據(jù)在主存和磁盤之間進(jìn)行交換安券。

虛擬地址翻譯

虛擬內(nèi)存的實(shí)現(xiàn)方式墩崩,大多數(shù)都是通過頁表來實(shí)現(xiàn)的。操作系統(tǒng)虛擬內(nèi)存空間分成一頁一頁的來管理侯勉,每頁的大小為 4K(當(dāng)然這是可以配置的鹦筹,不同操作系統(tǒng)不一樣)。磁盤和主內(nèi)存之間的置換也是以為單位來操作的址貌。4K 算是通過實(shí)踐折中出來的通用值盛龄,太小了會(huì)出現(xiàn)頻繁的置換,太大了又浪費(fèi)內(nèi)存。

虛擬地址 -> 物理地址 的映射關(guān)系由頁表(Page Table)記錄余舶,它其實(shí)就是一個(gè)數(shù)組,數(shù)組中每個(gè)元素叫做頁表?xiàng)l目(Page Table Entry锹淌,簡稱 PTE)匿值,PTE 由一個(gè)有效位和 n 位地址字段構(gòu)成,有效位標(biāo)識(shí)這個(gè)虛擬地址是否分配了物理內(nèi)存赂摆。

頁表被操作系統(tǒng)放在物理內(nèi)存的指定位置挟憔,CPU 上有個(gè) Memory Management Unit(MMU) 單元,CPU 把虛擬地址給 MMU烟号,MMU 去物理內(nèi)存中查詢頁表绊谭,得到實(shí)際的物理地址。當(dāng)然 MMU 不會(huì)每次都去查的汪拥,它自己也有一份緩存叫Translation Lookaside Buffer (TLB)达传,是為了加速地址翻譯。

虛擬地址翻譯

你慢慢會(huì)發(fā)現(xiàn)整個(gè)計(jì)算機(jī)體系里面迫筑,緩存是無處不在的宪赶,整個(gè)計(jì)算機(jī)體系就是建立在一級(jí)級(jí)的緩存之上的,無論軟硬件脯燃。

讓我們來看一下 CPU 內(nèi)存訪問的完整過程:

  1. CPU 使用虛擬地址訪問數(shù)據(jù)搂妻,比如執(zhí)行了 MOV 指令加載數(shù)據(jù)到寄存器,把地址傳遞給 MMU辕棚。
  2. MMU 生成 PTE 地址欲主,并從主存(或自己的 Cache)中得到它。
  3. 如果 MMU 根據(jù) PTE 得到真實(shí)的物理地址逝嚎,正常讀取數(shù)據(jù)扁瓢。流程到此結(jié)束。
  4. 如果 PTE 信息表示沒有關(guān)聯(lián)的物理地址懈糯,MMU 則觸發(fā)一個(gè)缺頁異常涤妒。
  5. 操作系統(tǒng)捕獲到這個(gè)異常,開始執(zhí)行異常處理程序赚哗。在物理內(nèi)存上創(chuàng)建一頁內(nèi)存她紫,并更新頁表。
  6. 缺頁處理程序在物理內(nèi)存中確定一個(gè)犧牲頁屿储,如果這個(gè)犧牲頁上有數(shù)據(jù)贿讹,則把數(shù)據(jù)保存到磁盤上。
  7. 缺頁處理程序更新 PTE够掠。
  8. 缺頁處理程序結(jié)束民褂,再回去執(zhí)行上一條指令(導(dǎo)致缺頁異常的那個(gè)指令,也就是 MOV 指令)。這次肯定命中了赊堪。

內(nèi)存命中率

你可能已經(jīng)發(fā)現(xiàn)面殖,上述的訪問步驟中,從第 4 步開始都是些很繁瑣的操作哭廉,頻繁的執(zhí)行對(duì)性能影響很大脊僚。畢竟訪問磁盤是非常慢的,它會(huì)引發(fā)程序性能的急劇下降遵绰。如果內(nèi)存訪問到第 3 步成功結(jié)束了辽幌,我們就說頁命中了;反之就是未命中椿访,或者說缺頁乌企,表示它開始執(zhí)行第 4 步了。

假設(shè)在 n 次內(nèi)存訪問中成玫,出現(xiàn)命中的次數(shù)是 m加酵,那么 m / n * 100% 就表示命中率,這是衡量內(nèi)存管理程序好壞的一個(gè)很重要的指標(biāo)梁剔。

如果物理內(nèi)存不足了虽画,數(shù)據(jù)會(huì)在主存和磁盤之間頻繁交換,命中率很低荣病,性能出現(xiàn)急劇下降码撰,我們稱這種現(xiàn)象叫內(nèi)存顛簸。這時(shí)你會(huì)發(fā)現(xiàn)系統(tǒng)的 swap 空間利用率開始增高个盆, CPU 利用率中 iowait 占比開始增高脖岛。

大多數(shù)情況下,只要物理內(nèi)存夠用颊亮,頁命中率不會(huì)非常低柴梆,不會(huì)出現(xiàn)內(nèi)存顛簸的情況。因?yàn)榇蠖鄶?shù)程序都有一個(gè)特點(diǎn)终惑,就是局部性绍在。

局部性就是說被引用過一次的存儲(chǔ)器位置,很可能在后續(xù)再被引用多次雹有;而且在該位置附近的其他位置偿渡,也很可能會(huì)在后續(xù)一段時(shí)間內(nèi)被引用。

前面說過計(jì)算機(jī)到處使用一級(jí)級(jí)的緩存來提升性能霸奕,歸根結(jié)底就是利用了局部性的特征溜宽,如果沒有這個(gè)特性,一級(jí)級(jí)的緩存不會(huì)有那么大的作用质帅。所以一個(gè)局部性很好的程序運(yùn)行速度會(huì)更快适揉。

CPU Cache

隨著技術(shù)發(fā)展留攒,CPU 的運(yùn)算速度越來越快,但內(nèi)存訪問的速度卻一直沒什么突破嫉嘀。最終導(dǎo)致了 CPU 訪問主存就成了整個(gè)機(jī)器的性能瓶頸炼邀。CPU Cache 的出現(xiàn)就是為了解決這個(gè)問題,在 CPU 和 主存之間再加了 Cache吃沪,用來緩存一塊內(nèi)存中的數(shù)據(jù)汤善,而且還不只一個(gè),現(xiàn)代計(jì)算機(jī)一般都有 3 級(jí) Cache票彪,其中 L1 Cache 的訪問速度和寄存器差不多。

現(xiàn)在訪問數(shù)據(jù)的大致的順序是 CPU --> L1 Cache --> L2 Cache --> L3 Cache --> 主存 --> 磁盤不狮。從左到右降铸,訪問速度越來越慢,空間越來越大摇零,單位空間(比如每字節(jié))的價(jià)格越來越低推掸。

現(xiàn)在存儲(chǔ)器的整體層次結(jié)構(gòu)大致如下圖:

存儲(chǔ)器層次結(jié)構(gòu)

在這種架構(gòu)下,緩存的命中率就更加重要了驻仅,因?yàn)橄到y(tǒng)會(huì)假定所有程序都是有局部性特征的谅畅。如果某一級(jí)出現(xiàn)了未命中,他就會(huì)將該級(jí)存儲(chǔ)的數(shù)據(jù)更新成最近使用的數(shù)據(jù)噪服。

主存與存儲(chǔ)器之間以 page(通常是 4K) 為單位進(jìn)行交換毡泻,cache 與 主存之間是以 cache line(通常 64 byte) 為單位交換的。

舉個(gè)例子

讓我們通過一個(gè)例子來驗(yàn)證下命中率的問題粘优,下面的函數(shù)是循環(huán)一個(gè)數(shù)組為每個(gè)元素賦值仇味。

func Loop(nums []int, step int) {
    l := len(nums)
    for i := 0; i < step; i++ {
        for j := i; j < l; j += step {
            nums[j] = 4
        }
    }
}

參數(shù) step 為 1 時(shí),和普通一層循環(huán)一樣雹顺。假設(shè) step 為 2 丹墨,則效果就是跳躍式遍歷數(shù)組,如 1,3,5,7,9,2,4,6,8,10 這樣嬉愧,step 越大贩挣,訪問跨度也就越大,程序的局部性也就越不好没酣。

下面是 nums 長度為 10000王财, step = 1step = 16 時(shí)的壓測結(jié)果:

goos: darwin
goarch: amd64
BenchmarkLoopStep1-4              300000              5241 ns/op
BenchmarkLoopStep16-4             100000             22670 ns/op

可以看出,2 種遍歷方式會(huì)出現(xiàn) 3 倍的性能差距四康。這種問題最容易出現(xiàn)在多維數(shù)組的處理上搪搏,比如遍歷一個(gè)二維數(shù)組很容易就寫出局部性很差的代碼。

程序的內(nèi)存布局

最后看一下程序的內(nèi)存布局∩两穑現(xiàn)在我們知道了每個(gè)程序都有自己一套獨(dú)立的地址空間可以使用疯溺,比如 0x0000 ~ 0xffff论颅,但我們?cè)谟酶呒?jí)語言,無論是 C 還是 Go 寫程序的時(shí)候囱嫩,很少直接使用這些地址恃疯。我們都是通過變量名來訪問數(shù)據(jù)的,編譯器會(huì)自動(dòng)將我們的變量名轉(zhuǎn)換成真正的虛擬地址墨闲。

那最終編譯出來的二進(jìn)制文件今妄,是如何被操作系統(tǒng)加載到內(nèi)存中并執(zhí)行的呢?

其實(shí)鸳碧,操作系統(tǒng)已經(jīng)將一整塊內(nèi)存劃分好了區(qū)域盾鳞,每個(gè)區(qū)域用來做不同的事情。如圖:

內(nèi)存布局
  • text 段:存儲(chǔ)程序的二進(jìn)制指令瞻离,及其他的一些靜態(tài)內(nèi)容
  • data 段:用來存儲(chǔ)已被初始化的全局變量腾仅。比如常量(const)。
  • bss 段:用來存放未被初始化的全局變量套利。和 .data 段一樣都屬于靜態(tài)分配推励,在這里面的變量數(shù)據(jù)在編譯就確定了大小,不釋放肉迫。
  • stack 段:椦榇牵空間,主要用于函數(shù)調(diào)用時(shí)存儲(chǔ)臨時(shí)變量的喊衫。這部分的內(nèi)存是自動(dòng)分配自動(dòng)釋放的跌造。
  • heap 段:堆空間,用于動(dòng)態(tài)分配格侯,C 語言中 mallocfree 操作的內(nèi)存就在這里鼻听;Go 語言主要靠 GC 自動(dòng)管理這部分。

其實(shí)現(xiàn)在的操作系統(tǒng)联四,進(jìn)程內(nèi)部的內(nèi)存區(qū)域沒這么簡單撑碴,要比這復(fù)雜多了,比如內(nèi)核區(qū)域朝墩,共享庫區(qū)域醉拓。因?yàn)槲覀儾皇且娴拈_發(fā)一套操作系統(tǒng),細(xì)節(jié)可以忽略收苏。這里只需要記住堆空間椧诼保空間即可。

  • 椔拱裕空間是通過壓棧出棧方式自動(dòng)分配釋放的排吴,由系統(tǒng)管理,使用起來高效無感知懦鼠。
  • 堆空間是用以動(dòng)態(tài)分配的钻哩,由程序自己管理分配和釋放屹堰。Go 語言雖然可以幫我們自動(dòng)管理分配和釋放,但是代價(jià)也是很高的街氢。

結(jié)論

局部性好的程序扯键,可以提高緩存命中率,這對(duì)底層系統(tǒng)的內(nèi)存管理是很友好的珊肃,可以提高程序的性能荣刑。CPU Cache 層面的低命中率導(dǎo)致的是程序運(yùn)行緩慢,內(nèi)存層面的低命中率會(huì)出現(xiàn)內(nèi)存顛簸伦乔,出現(xiàn)這種現(xiàn)象時(shí)你的服務(wù)基本上已經(jīng)癱瘓了厉亏。Go 語言的內(nèi)存管理是參考 tcmalloc 實(shí)現(xiàn)的,它其實(shí)就是利用好了 OS 管理內(nèi)存的這些特點(diǎn)烈和,來最大化內(nèi)存分配性能的叶堆。

參考

  1. Go Memory Management
  2. 《深入理解計(jì)算機(jī)系統(tǒng)》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市斥杜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沥匈,老刑警劉巖蔗喂,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異高帖,居然都是意外死亡缰儿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門散址,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乖阵,“玉大人,你說我怎么就攤上這事预麸〉山” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵吏祸,是天一觀的道長对蒲。 經(jīng)常有香客問我,道長贡翘,這世上最難降的妖魔是什么蹈矮? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鸣驱,結(jié)果婚禮上泛鸟,老公的妹妹穿的比我還像新娘。我一直安慰自己踊东,他們只是感情好北滥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布刚操。 她就那樣靜靜地躺著,像睡著了一般碑韵。 火紅的嫁衣襯著肌膚如雪赡茸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天祝闻,我揣著相機(jī)與錄音占卧,去河邊找鬼。 笑死联喘,一個(gè)胖子當(dāng)著我的面吹牛华蜒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播豁遭,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼叭喜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蓖谢?” 一聲冷哼從身側(cè)響起捂蕴,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闪幽,沒想到半個(gè)月后啥辨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盯腌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年溉知,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腕够。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡级乍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出帚湘,到底是詐尸還是另有隱情玫荣,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布客们,位于F島的核電站崇决,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏底挫。R本人自食惡果不足惜恒傻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望建邓。 院中可真熱鬧盈厘,春花似錦、人聲如沸官边。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至契吉,卻和暖如春跳仿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捐晶。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工菲语, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惑灵。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓山上,卻偏偏與公主長得像,于是被迫代替她去往敵國和親英支。 傳聞我的和親對(duì)象是個(gè)殘疾皇子佩憾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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

  • 本文轉(zhuǎn)載自 https://juejin.im/post/59f8691b51882534af254317 參考:...
    xingdong閱讀 2,711評(píng)論 0 3
  • 什么是MMU MMU(Memory Management Unit)主要用來管理虛擬存儲(chǔ)器、物理存儲(chǔ)器的控制線路干花,...
    放風(fēng)箏的小小馬閱讀 5,742評(píng)論 1 7
  • 1. 基礎(chǔ)知識(shí) 1.1妄帘、 基本概念、 功能 馮諾伊曼體系結(jié)構(gòu)1池凄、計(jì)算機(jī)處理的數(shù)據(jù)和指令一律用二進(jìn)制數(shù)表示2寄摆、順序執(zhí)...
    yunpiao閱讀 5,253評(píng)論 1 22
  • 在linux下,使用top,free等命令查看系統(tǒng)或者進(jìn)程的內(nèi)存使用情況時(shí)修赞,經(jīng)常看到buff/cache meme...
    analanxingde閱讀 698評(píng)論 0 2
  • 操作系統(tǒng)對(duì)內(nèi)存的管理 沒有內(nèi)存抽象的年代 在早些的操作系統(tǒng)中桑阶,并沒有引入內(nèi)存抽象的概念柏副。程序直接訪問和操作的都是物...
    Mr槑閱讀 16,662評(píng)論 3 24