<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          圖解 Go 內(nèi)存管理器的內(nèi)存分配策略

          共 4090字,需瀏覽 9分鐘

           ·

          2022-05-22 08:36

          關(guān)于Go的內(nèi)存分配

          Go語(yǔ)言里,從內(nèi)存的分配到不再使用后內(nèi)存的回收等等這些內(nèi)存管理工作都是由Go在底層完成的。雖然開(kāi)發(fā)者在寫(xiě)代碼時(shí)不必過(guò)度關(guān)心內(nèi)存從分配到回收這個(gè)過(guò)程,但是Go的內(nèi)存分配策略里有不少有意思的設(shè)計(jì),通過(guò)了解他們有助于我們自身的提高,也讓我們能寫(xiě)出更高效的Go程序。

          Go內(nèi)存管理的設(shè)計(jì)旨在在并發(fā)環(huán)境中快速運(yùn)行,并與垃圾回收器集成在一起。讓我們看一個(gè)簡(jiǎn)單的示例:

          package?main

          type?smallStruct?struct?{
          ???a,?b?int64
          ???c,?d?float64
          }

          func?main()?{
          ???smallAllocation()
          }

          //go:noinline
          func?smallAllocation()?*smallStruct?{
          ???return?&smallStruct{}
          }

          函數(shù)上面的注釋//go:noinline將禁止Go對(duì)該函數(shù)進(jìn)行內(nèi)聯(lián),這樣main函數(shù)就會(huì)使用smallAllocation函數(shù)返回的指針變量,因?yàn)楸欢鄠€(gè)函數(shù)使用,返回的這個(gè)變量將被分配到堆上。

          關(guān)于內(nèi)聯(lián)的概念之前的文章有說(shuō)過(guò):

          內(nèi)聯(lián)是一種手動(dòng)或編譯器優(yōu)化,用于將簡(jiǎn)短函數(shù)的調(diào)用替換為函數(shù)體本身。這么做的原因是它可以消除函數(shù)調(diào)用本身的開(kāi)銷(xiāo),也使得編譯器能更高效地執(zhí)行其他的優(yōu)化策略。

          所以如果上面的例子不干預(yù)編譯器的話(huà),編譯器通過(guò)內(nèi)聯(lián)將smallAllocation函數(shù)體里的內(nèi)容直接放到main函數(shù)里,這樣就不會(huì)產(chǎn)生smallAllocation這個(gè)函數(shù)的調(diào)用了,所有的變量都是main函數(shù)內(nèi)這個(gè)范圍使用的,也就不在需要將變量往堆上分配了。

          繼續(xù)說(shuō)上面那個(gè)例子,通過(guò)逃逸分析命令 go tool compile ?-m main.go 可以確認(rèn)我們上面的分析,&smallStruct{}會(huì)被分配到堆上去。

          ??go?tool?compile?-m?main.go
          main.go:12:6:?can?inline?main
          main.go:10:9:?&smallStruct?literal?escapes?to?heap

          借助命令go tool compile -S main.go,可以顯示該程序的匯編代碼,也可以明確地向我們展示內(nèi)存的分配:

          0x001d?00029?(main.go:10)???????LEAQ????type."".smallStruct(SB),?AX
          0x0024?00036?(main.go:10)???????PCDATA??$2,?$0
          0x0024?00036?(main.go:10)???????MOVQ????AX,?(SP)
          0x0028?00040?(main.go:10)???????CALL????runtime.newobject(SB)

          內(nèi)置函數(shù)newobject會(huì)通過(guò)調(diào)用另外一個(gè)內(nèi)置函數(shù)mallocgc在堆上分配新內(nèi)存。在Go里面有兩種內(nèi)存分配策略,一種適用于程序里小內(nèi)存塊的申請(qǐng),另一種適用于大內(nèi)存塊的申請(qǐng),大內(nèi)存塊指的是大于32KB。

          下面我們來(lái)細(xì)聊一下這兩種策略。

          小于32KB內(nèi)存塊的分配策略

          當(dāng)程序里發(fā)生了32kb以下的小塊內(nèi)存申請(qǐng)時(shí),Go會(huì)從一個(gè)叫做的mcache的本地緩存給程序分配內(nèi)存。這個(gè)本地緩存mcache持有一系列的大小為32kb的內(nèi)存塊,這樣的一個(gè)內(nèi)存塊里叫做mspan,它是要給程序分配內(nèi)存時(shí)的分配單元。

          從mcache中給程序分配內(nèi)存

          在Go的調(diào)度器模型里,每個(gè)線(xiàn)程M會(huì)綁定給一個(gè)處理器P,在單一粒度的時(shí)間里只能做多處理運(yùn)行一個(gè)goroutine,每個(gè)P都會(huì)綁定一個(gè)上面說(shuō)的本地緩存mcache。當(dāng)需要進(jìn)行內(nèi)存分配時(shí),當(dāng)前運(yùn)行的goroutine會(huì)從mcache中查找可用的mspan。從本地mcache里分配內(nèi)存時(shí)不需要加鎖,這種分配策略效率更高。

          那么有人就會(huì)問(wèn)了,有的變量很小就是數(shù)字,有的卻是一個(gè)復(fù)雜的結(jié)構(gòu)體,申請(qǐng)內(nèi)存時(shí)都分給他們一個(gè)mspan這樣的單元會(huì)不會(huì)產(chǎn)生浪費(fèi)。其實(shí)mcache持有的這一系列的mspan并不都是統(tǒng)一大小的,而是按照大小,從8字節(jié)到32KB分了大概70類(lèi)的msapn

          按照大小分類(lèi)的mspan

          就文章開(kāi)始的那個(gè)例子來(lái)說(shuō),那個(gè)結(jié)構(gòu)體的大小是32字節(jié),正好32字節(jié)的這種mspan能滿(mǎn)足需求,那么分配內(nèi)存的時(shí)候就會(huì)給它分配一個(gè)32字節(jié)大小的mspan

          alloc 分配內(nèi)存

          現(xiàn)在,我們可能會(huì)好奇,如果分配內(nèi)存時(shí)mcachce里沒(méi)有空閑的32字節(jié)的mspan了該怎么辦?Go里還為每種類(lèi)別的mspan維護(hù)著一個(gè)mcentral

          mcentral的作用是為所有mcache提供切分好的mspan資源。每個(gè)central會(huì)持有一種特定大小的全局mspan列表,包括已分配出去的和未分配出去的。每個(gè)mcentral對(duì)應(yīng)一種mspan,當(dāng)工作線(xiàn)程的mcache中沒(méi)有合適(也就是特定大小的)的mspan時(shí)就會(huì)從mcentral 去獲取。mcentral被所有的工作線(xiàn)程共同享有,存在多個(gè)goroutine競(jìng)爭(zhēng)的情況,因此從mcentral獲取資源時(shí)需要加鎖。

          mcentral的定義如下:

          //runtime/mcentral.go

          type?mcentral?struct?{
          ????//?互斥鎖
          ????lock?mutex?
          ????
          ????//?規(guī)格
          ????sizeclass?int32?
          ????
          ????//?尚有空閑object的mspan鏈表
          ????nonempty?mSpanList?
          ????
          ????//?沒(méi)有空閑object的mspan鏈表,或者是已被mcache取走的msapn鏈表
          ????empty?mSpanList?
          ????
          ????//?已累計(jì)分配的對(duì)象個(gè)數(shù)
          ????nmalloc?uint64?
          }

          mcentral里維護(hù)著兩個(gè)雙向鏈表,nonempty表示鏈表里還有空閑的mspan待分配。empty表示這條鏈表里的mspan都被分配了object

          mcentral

          如果上面我們那個(gè)程序申請(qǐng)內(nèi)存的時(shí)候,mcache里已經(jīng)沒(méi)有合適的空閑mspan了,那么工作線(xiàn)程就會(huì)像下圖這樣去mcentral里去申請(qǐng)。

          簡(jiǎn)單說(shuō)下mcachemcentral獲取和歸還mspan的流程:

          • 獲取 加鎖;從nonempty鏈表找到一個(gè)可用的mspan;并將其從nonempty鏈表刪除;將取出的mspan加入到empty鏈表;將mspan返回給工作線(xiàn)程;解鎖。
          • 歸還 加鎖;將mspanempty鏈表刪除;將mspan加入到nonempty鏈表;解鎖。
          從mcentral里申請(qǐng)mspan

          當(dāng)mcentral沒(méi)有空閑的mspan時(shí),會(huì)向mheap申請(qǐng)。而mheap沒(méi)有資源時(shí),會(huì)向操作系統(tǒng)申請(qǐng)新內(nèi)存。mheap主要用于大對(duì)象的內(nèi)存分配,以及管理未切割的mspan,用于給mcentral切割成小對(duì)象。

          從heap上申請(qǐng)內(nèi)存

          同時(shí)我們也看到,mheap中含有所有規(guī)格的mcentral,所以,當(dāng)一個(gè)mcachemcentral申請(qǐng)mspan時(shí),只需要在獨(dú)立的mcentral中使用鎖,并不會(huì)影響申請(qǐng)其他規(guī)格的mspan

          上面說(shuō)了每種尺寸的mspan都有一個(gè)全局的列表存放在mcentral里供所有線(xiàn)程使用,所有mcentral的集合則是存放于mheap中的。mheap里的arena 區(qū)域是真正的堆區(qū),運(yùn)行時(shí)會(huì)將 8KB 看做一頁(yè),這些內(nèi)存頁(yè)中存儲(chǔ)了所有在堆上初始化的對(duì)象。運(yùn)行時(shí)使用二維的 runtime.heapArena 數(shù)組管理所有的內(nèi)存,每個(gè) runtime.heapArena 都會(huì)管理 64MB 的內(nèi)存。

          如果 arena 區(qū)域沒(méi)有足夠的空間,會(huì)調(diào)用 runtime.mheap.sysAlloc 從操作系統(tǒng)中申請(qǐng)更多的內(nèi)存。

          大于32KB內(nèi)存塊的分配策略

          Go沒(méi)法使用工作線(xiàn)程的本地緩存mcache和全局中心緩存mcentral上管理超過(guò)32KB的內(nèi)存分配,所以對(duì)于那些超過(guò)32KB的內(nèi)存申請(qǐng),會(huì)直接從堆上(mheap)上分配對(duì)應(yīng)的數(shù)量的內(nèi)存頁(yè)(每頁(yè)大小是8KB)給程序。

          直接從堆上分配內(nèi)存

          總結(jié)

          我們把內(nèi)存分配管理涉及的所有概念串起來(lái),可以勾畫(huà)出Go內(nèi)存管理的一個(gè)全局視圖:

          Go內(nèi)存分配的全局示意圖

          Go語(yǔ)言的內(nèi)存分配非常復(fù)雜,這個(gè)文章從一個(gè)比較粗的角度來(lái)看Go的內(nèi)存分配,并沒(méi)有深入細(xì)節(jié)。一般而言,了解它的原理,到這個(gè)程度也就可以了(應(yīng)付面試)。

          總結(jié)起來(lái)關(guān)于Go內(nèi)存分配管理的策略有如下幾點(diǎn):

          • Go在程序啟動(dòng)時(shí),會(huì)向操作系統(tǒng)申請(qǐng)一大塊內(nèi)存,由mheap結(jié)構(gòu)全局管理。
          • Go內(nèi)存管理的基本單元是mspan,每種mspan可以分配特定大小的object
          • mcache, mcentral, mheapGo內(nèi)存管理的三大組件,mcache管理線(xiàn)程在本地緩存的mspanmcentral管理全局的mspan供所有線(xiàn)程使用;mheap管理Go的所有動(dòng)態(tài)分配內(nèi)存。
          • 一般小對(duì)象通過(guò)mspan分配內(nèi)存;大對(duì)象則直接由mheap分配內(nèi)存。

          相關(guān)閱讀

          Go內(nèi)存管理之代碼的逃逸分析

          上周并發(fā)題的解題思路以及介紹Go語(yǔ)言調(diào)度器

          參考鏈接

          Memory Management and Allocation[1]

          圖解Go語(yǔ)言?xún)?nèi)存分配[2]

          內(nèi)存分配器[3]

          參考資料

          [1]

          Memory Management and Allocation: https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44

          [2]

          圖解Go語(yǔ)言?xún)?nèi)存分配: https://juejin.im/post/6844903795739082760#heading-7

          [3]

          內(nèi)存分配器: https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/


          - END -


          關(guān)注公眾號(hào),獲取更多精選技術(shù)原創(chuàng)文章

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人少妇网站 | 五月麻豆 | 亚洲视频在线观看免费 | 97人人爽人人爽人人爽人人爽 | 天天操夜操 |