# Go堆內存管理
1. Go內存模型層級結構
Golang內存管理模型與TCMalloc的設計極其相似。基本輪廓和概念也幾乎相同,只是一些規則和流程存在差異。
2. Go內存管理的基本概念
Go內存管理的許多概念在TCMalloc中已經有了,含義是相同的,只是名字有一些變化。
2.1 Page
與TCMalloc中的Page相同,x64架構下1個Page的大小是8KB。Page表示Golang內存管理與虛擬內存交互內存的最小單元。操作系統虛擬內存對于Golang來說,依然是劃分成等分的N個Page組成的一塊大內存公共池。
2.2 mspan
與TCMalloc中的Span一致。mspan概念依然延續TCMalloc中的Span概念,在Golang中將Span的名稱改為mspan,1個mspan為多個Page(go中為8KB的內存大小)。1個mspan對應1個或多個大小相同的object,mspan主要用于分配對象的區塊,下圖簡單說明了Span的內部結構。
mspan結構體如下:
type mspan struct {next *mspan // 在mspan鏈表中,指向后一個mspanprev *mspan // 在mspan鏈表中,指向前一個mspanlist *mSpanList // 供debug使用startAddr uintptr // mspan起始地址npages uintptr // 當前mspan對應的page數manualFreeList gclinkptr // mSpanManual狀態mspan中的可用對象鏈表// freeindex是slot索引,標記下一次分配對象時應該開始搜索的地址, 分配后freeindex會增加// 每一次分配都從freeindex開始掃描allocBits,直到它遇到一個表示空閑對象的0// 在freeindex之前的元素都是已分配的, 在freeindex之后的元素有可能已分配, 也有可能未分配freeindex uintptrnelems uintptr // 當前span中object數量.// allocCache是從freeindex位置開始的allocBits緩存allocCache uint64// allocBits用于標記哪些元素是已分配的, 哪些元素是未分配的。// 使用freeindex + allocBits可以在分配時跳過已分配的元素, 把對象設置在未分配的元素中.allocBits *gcBits// 用于在gc時標記哪些對象存活, 每次gc以后allocBits都會與gcmarkBits保持一致gcmarkBits *gcBits// 清理代數,每GC1次sweepgen會+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 uint32divMul uint32 // for divide by elemsizeallocCount uint16 // 已分配對象的數量spanclass spanClassstate mSpanStateBoxneedzero uint8 // 在分配前需要清零elemsize uintptr // 對象大小limit uintptr // span數據末尾speciallock mutex // specials鏈表的鎖specials *special // 根據object偏移量排序的special鏈表.}
mspan的allocBits是一個bitmap,用于標記哪些元素是已分配的, 哪些元素是未分配的。通過使用allocBits已經可以達到O(1)的分配速度,但是go為了極限性能,對其做了一個緩存allocCache,allocCache是從freeindex開始的allocBits緩存。
2.3 Size Class
Golang內存管理針對衡量內存的概念又更加詳細了很多,這里面介紹一些基礎的有關內存大小的名詞及算法。
-
Object Class
是指協程應用邏輯一次向Go內存申請的對象Object大小。Object是Golang內存管理模塊針對內存管理更加細化的內存管理單元。一個Span在初始化時會被分成多個Object。比如Object Size是8B(8字節)大小的Object,所屬的Span大小是8KB(8192字節),那么這個Span就會被平均分割成1024(8192/8=1024)個Object。
邏輯層從Golang內存模型取內存,實則是分配一個Object出去。為了更好的讓讀者理解,這里假設了幾個數據來標識Object Size 和Span的關系 ,如下圖所示。
Page是Golang內存管理與操作系統交互時,衡量內存容量的基本單元
Object是用來存儲一個變量數據的內存空間, 是Golang內存管理為對象分配存儲內存的基本單元
Size Class
是指Object大小的級別。比如Object Size在1Byte~8Byte之間的Object屬于Size Class 1級別,Object Size 在8B~16Byte之間的屬于Size Class 2級別。本質上,golang的Size Class與TCMalloc中size class都是表示一塊內存的所屬規格。
go中共存在
_NumSizeClasses = 68
個Size Class(0~68),所以也對應著68個Object Class
Span Class
是Golang內存管理額外定義的規格屬性,也是針對Object大小來進行劃分的。但是為了優化GC Mark階段,go內部讓一個Size Class對應2個Span Class,其中一個Span為存放需要GC掃描的對象(包含指針的對象, scan span),另一個Span為存放不需要GC掃描的對象(不包含指針的對象, noscan span)。
通過設置兩種span,讓GC掃描對象的時候,對于noscan的span可以不去查看bitmap區域來標記子對象。也就是說進行掃描的時候,直接判定該span中的對象不會存在引用對象,不再進行更深層的掃描,這樣可以大幅提升GC Mark的效率。
具體Span Class與Size Class的邏輯結構關系如下圖所示。
其中Size Class和Span Class的對應關系計算方式可以參考Golang源代碼,如下:
//usr/local/go/src/runtime/mheap.gotype spanClass uint8 //……(省略部分代碼)func makeSpanClass(sizeclass uint8, noscan bool) spanClass {return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))}//……(省略部分代碼)
makeSpanClass()函數為通過Size Class來得到對應的Span Class,其中第二個形參noscan表示當前對象是否需要GC掃描
,不難看出來Span Class 和Size Class的對應關系公式如下表所示:
| 對象 | Size Class 與 Span Class對應公式 |
| ---------------------------- | -------------------------------- |
| 需要GC掃描是否存在引用對象 | Span Class = Size Class * 2 + 0 |
| 不需要GC掃描是否存在引用對象 | Span Class = Size Class * 2 + 1 |
Golang源碼里列舉了詳細的Size Class和Object大小、存放Object數量,以及每個Size Class對應的Span內存大小關系,我們這里只展示部分:
//usr/local/go/src/runtime/sizeclasses.gopackage runtime// [class]: Size Class// [bytes/obj]: Object Size,一次對外提供內存Object的大小// [bytes/span]: 當前Object所對應Span的內存大小// [objects]: 當前Span一共有多少個Object// [tail waste]: 當前Span平均分N份Object后,會有多少內存浪費。 ===> [bytes/span]%[bytes/obj]// [max waste]: 當前Size Class最大可能浪費的空間所占百分比。 ===> ((本級Object Size – (上級Object Size + 1))*本級Object數量) + [tail waste])/ 本級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的規格刻度內存。對于Span Class為0和1的,也就是對應Size Class為0的規格刻度內存,mcache實際上是沒有分配任何內存的。因為Golang內存管理對內存為0的數據申請做了特殊處理,如果申請的數據大小為0將直接返回一個固定內存地址,不會走Golang內存管理的正常邏輯,詳見以下源碼
//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)}//……(省略部分代碼)}
上述代碼可以看見,如果申請的size為0,則直接return一個固定地址**zerobase
**。所以在68種Size Class中,執行newobject時,會申請內存的Size Class為67種。在Golang中如[0]int、 struct{}所需要內存大小均是0,這也是為什么很多開發者在通過Channel做同步時,發送一個struct{}數據,因為不會申請任何內存,能夠適當節省一部分內存空間。
golang中[0]int、 struct{}等,全部的0內存對象分配,返回的都是一個固定的地址。
max waste為當前Size Class最大可能浪費的空間所占百分比計算方式,詳見下圖
2.4 MCache
mcache與TCMalloc中的ThreadCache類似,但也有所不同。
相同點:都保存的是各種大小的Span,并按Span class分類,小對象直接從此分配內存,起到了緩存的作用,并且可以無鎖訪問
不同點:TCMalloc中是1個線程1個ThreadCache,Go中是1個P擁有1個mcache,兩者綁定關系的區別如下圖所示
如果將上圖的mcache展開,來看mcache的內部構造,則具體的結構形式如下圖6所示
當其中某個Span Class的MSpan已經沒有可提供的Object時,MCache則會向MCentral申請一個對應的MSpan。mcache在初始化時是沒有任何mspan資源的,在使用過程中會動態地申請,不斷地去填充 alloc[numSpanClasses]*mspan,通過雙向鏈表連接。
下面具體看一下mcache在源碼中的定義:
//go:notinheaptype mcache struct { tiny uintptr //<16byte 申請小對象的起始地址tinyoffset uintptr //從起始地址tiny開始的偏移量local_tinyallocs uintptr //tiny對象分配的數量 alloc [numSpanClasses]*mspan // 分配的mspan list,其中numSpanClasses=67*2,索引是splanclassIdstackcache [_NumStackOrders]stackfreelist //棧緩存local_largefree uintptr // 大對象釋放字節數local_nlargefree uintptr // 釋放的大對象數量local_nsmallfree [_NumSizeClasses]uintptr // 每種規格小對象釋放的個數flushGen uint32 //掃描計數}
MCache中每個Span Class都只會對應一個MSpan對象,不同Span Class的MSpan的總體長度不同,參考runtime/sizeclasses.go的標準規定劃分。比如對于Span Class為4的MSpan來說,存放內存大小為1Page,即8KB。每個對外提供的Object大小為16B,共存放512個Object。其他Span Class的存放方式類似。
通過源碼可以看到MCache通過alloc[numSpanClasses]mspan管理了很多不同規格不同類型的span,golang對于*[16B,32KB]
**的對象會使用這部分span進行內存分配,所有在這區間大小的對象都會從alloc這個數組里尋找。
var sizeclass uint8//確定規格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]
而對于更小的對象,我們叫它tiny對象,golang會通過tiny和tinyoffset組合尋找位置分配內存空間,這樣可以更好的節約空間,源碼如下:
off := c.tinyoffset//根據不同大小內存對齊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 + sizec.local_tinyallocs++mp.mallocing = 0releasem(mp)return x}// 空間不足從alloc重新申請空間用于tiny對象分配span := c.alloc[tinySpanClass]
2.5 MCentral
MCentral與TCMalloc中的Central概念依然相似。向MCentral申請Span是同樣是需要加鎖的。
當MCache的某個級別Span的內存被分配光時,它會向MCentral申請1個當前級別的Span。
Goroutine、MCache、MCentral、MHeap互相交換的內存單位是不同,其中協程邏輯層與MCache的內存交換單位是Object,MCache與MCentral、MCentral與MHeap的內存交換單位是Span,MHeap與操作系統的內存交換單位是Page。
MCentral與TCMalloc中的Central不同的是:CentralCache是每個級別的Span有1個鏈表,mcache是每個級別的Span有2個鏈表。如下圖所示。
MCentral屬于MHeap,MCentral是各個規格的mcentral集合,實際上1個mcentral對應1個Span Class,即Span Class個mcentral小內存管理單元。對應源碼為:
type mheap struct {......central [numSpanClasses]struct {mcentral mcentralpad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte}......}
-
NonEmpty Span List
表示還有可用空間的Span鏈表。鏈表中的所有Span都至少有1個空閑的Object空間。如果MCentral上游MCache退還Span,會將退還的Span加入到NonEmpty Span List鏈表中。
-
Empty Span List
表示沒有可用空間的Span鏈表。該鏈表上的Span都不確定是否存在空閑的Object空間。如果MCentral提供給一個Span給到上游MCache,那么被提供的Span就會加入到Empty List鏈表中。
注意 在Golang 1.16版本之后,MCentral中的NonEmpty Span List 和 Empty Span List
均由鏈表管理改成集合管理,分別對應Partial Span Set 和 Full Span Set。雖然存儲的數據結構有變化,但是基本的作用和職責沒有區別。
下面是MCentral層級中其中一個Size Class級別的MCentral的定義Golang源代碼(V1.14版本):
//usr/local/go/src/runtime/mcentral.go , Go V1.14// Central list of free objects of a given size.// go:notinheaptype mcentral struct {lock mutex //申請MCentral內存分配時需要加的鎖spanclass spanClass //當前哪個Size Class級別的// 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鏈表,或者當前鏈表里的Span已經交給mcacheempty 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分配的對象的累積計數// 假設mcaches中的所有跨度都已完全分配。// 以原子方式書寫,在STW下閱讀。nmalloc uint64}
在GolangV1.16版本的相關MCentral結構代碼如下:
//usr/local/go/src/runtime/mcentral.go , Go V1.16+//…type mcentral struct {// mcentral對應的spanClassspanclass spanClasspartial [2]spanSet // 維護全部空閑的Span集合full [2]spanSet // 維護存在非空閑的Span集合}//…
新版本的改進是將List變成了兩個Set集合,Partial集合與NonEmpty Span List責任類似,Full集合與Empty Span List責任類似。可以看見Partial和Full都是一個[2]spanSet類型,也就每個Partial和Full都各有兩個spanSet集合,這是為了給GC垃圾回收來使用的,其中一個集合是已掃描的,另一個集合是未掃描的。
2.6 MHeap
Golang內存管理的MHeap依然是繼承TCMalloc的PageHeap設計。MHeap的上游是MCentral,MCentral中的Span不夠時會向MHeap申請。MHeap的下游是操作系統,MHeap的內存不夠時會向操作系統的虛擬內存空間申請。訪問MHeap獲取內存依然是需要加鎖的。
MHeap是對內存塊的管理對象,是通過Page為內存單元進行管理。那么用來詳細管理每一系列Page的結構稱之為一個HeapArena,它們的邏輯層級關系如下圖所示。
一個HeapArena占用內存64MB,其中里面的內存的是一個一個的mspan,當然最小單元依然是Page,圖中沒有表示出mspan,因為多個連續的page就是一個mspan。所有的HeapArena組成的集合是一個arenas [1]*[4M]*heapArena數組,運行時使用arenas 管理所有的內存。
mheap是Golang進程全局唯一的,所以訪問依然加鎖。圖中又出現了mcentral,因為mcentral本也屬于mheap中的一部分。只不過會優先從MCentral獲取內存,如果沒有mcentral會從Arenas中的某個heapArena獲取Page。
heapArena結構體如下:
type heapArena struct { bitmap [heapArenaBitmapBytes]byte // 用于標記當前這個HeapArena的內存使用情況,1. 對應地址中是否存在過對象、對象中哪些地址包含指針,2. 是否被GC標記過。主要用于GCspans [pagesPerArena]*mspan // 存放heapArena中的span指針地址pageInUse [pagesPerArena / 8]uint8 // 保存哪些spans處于mSpanInUse狀態pageMarks [pagesPerArena / 8]uint8 // 保存哪些spans中包含被標記的對象pageSpecials [pagesPerArena / 8]uint8 // 保存哪些spans是特殊的checkmarks *checkmarksMap // debug.gccheckmark statezeroedBase uintptr //該arena第一頁的第一個字節地址}
根據heapArena結構體,我們可以了解到mheap內存空間的邏輯視圖如下所示:
其中arena區域就是我們通常說的heap, go從heap分配的內存都在這個區域中。
其中spans區域用于表示arena區中的某一頁(Page)屬于哪個span,spans區域中一個指針(8 byte)對應了arena區域中的一頁(在go中一頁=8KB)。所以spans的大小是 512GB / 頁大小(8KB) * 指針大小(8 byte) = 512MB。spans區域和arenas區域的對應關系如下圖所示:
其中每個HeapArean包含一個bitmap,其作用是用于標記當前這個HeapArena的內存使用情況。
1個bitmap的邏輯結構圖如下所示:
1個bitmap是8bit,每一個指針大小的內存都會有兩個bit分別表示是否應該繼續掃描和是否包含指針,這樣1個byte就會對應arena區域的四個指針大小的內存。當前HeapArena中的所有Page均會被bitmap所標記,bitmap的主要作用是服務于GC垃圾回收模塊。
bitmap中的byte和arena的對應關系從末尾開始, 也就是隨著內存分配會向兩邊擴展
MHeap里面相關的數據結構和指針依賴關系,可以參考下圖:
mheap結構體如下:
type mheap struct {lock mutex //必須在系統堆棧上獲得,否則當G持有鎖時,堆棧增長,可能會自我死鎖pages pageAlloc // page分配器數據結構sweepgen uint32 // 記錄span的sweep及cache狀態sweepDrained uint32 // 所有的span都已被清掃,或都正在被清掃sweepers uint32 // 啟動的swepper數量allspans []*mspan // 曾經創建的所有mspans地址的切片,allspans的內存是手動管理的,可以隨著堆的增長而重新分配和移動。// 一般來說,allspans受到mheap_.lock的保護,它可以防止并發訪問以及釋放后備存儲。// 在STW期間的訪問可能不會持有鎖,但必須確保訪問周圍不能發生分配(因為這可能會釋放支持存儲)。pagesInUse uint64 // pages所屬的spans處于狀態mSpanInUse; 原子式更新pagesSwept uint64 // 本周期內被清掃的pages數; 原子式更新pagesSweptBasis uint64 // 被用作Proportional sweep模式原點的pagesSwept; 原子式更新sweepHeapLiveBasis uint64 // gcController.heapLive的值,作為掃描率的原點;帶鎖寫入,不帶鎖讀取。sweepPagesPerByte float64 // Proportional sweep比例; 寫時有鎖,讀時無鎖// TODO(austin): pagesInUse should be a uintptr, but the 386 compiler can't 8-byte align fields.scavengeGoal uint64 // 維持的總的保留堆內存量(運行時試圖通過向操作系統返回內存來維持該內存量,該內存量由heapRetained衡量)。reclaimIndex uint64 // 下一個要回收的page在allArenas中的索引reclaimCredit uintptr// arenas是*heapArena的map. 它指向整個可用的虛擬地址空間的每一個arena幀的堆的元數據。// 這是一個兩級映射,由一個L1映射和可能的許多L2映射組成。當有大量的arena時,這可以節省空間arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArenaheapArenaAlloc linearAlloc // 用于分配heapArena對象的預留空間。這只在32位上使用,我們預先保留這個空間以避免與堆本身交錯。arenaHints *arenaHint // arenaHints是一個地址列表,用于標記哪里的heap arenas需要擴容arena linearAlloc // 是一個預先保留的空間,用于分配heap arenas。只用在32位操作系統allArenas []arenaIdx // 所有arena序號集合,可以根據arenaIdx算出對應arenas中的那一個heapArenasweepArenas []arenaIdx // sweepArenas是在掃描周期開始時對所有Arenas的快照,通過禁用搶占可以安全讀取markArenas []arenaIdx // markArenas是在標記周期開始時對所有Arenas的快照,由于allArenas只可向后追加,并且標記不會修改該切片內容,所以可以安全讀取//curArena是堆當前正在擴容的區域,curArena總是與physPageSize對齊curArena struct {base, end uintptr}// central 是存放small size classes的列表central [numSpanClasses]struct {mcentral mcentral// pad確保mcentrals間隔CacheLinePadSize字節,以便每個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 specialReachablespeciallock mutex // lock for special record allocators.arenaHintAlloc fixalloc // allocator for arenaHintsunused *specialfinalizer // never set, just here to force the specialfinalizer type into DWARF}
arenaHint結構體為:
type arenaHint struct {addr uintptr // 為指向的對應heapArena首地址。down bool // 為當前的heapArena是否可以擴容。next *arenaHint // 指向下一個heapArena所對應的ArenaHint首地址。}
3. 內存分配規則
介紹完內存管理基本概念,我們再來總結一下內存分配規則,流程圖如下:
3.1 Tiny對象分配流程
-
判斷對象大小是否小于maxSmallSize=32KB,如果小于32KB則進入Tiny對象或小對象申請流程,否則進入大對象申請流程。
-
判斷對象大小是否小于maxTinySize=16B并且對象中是否包含指針,如果大于16B或包含指針,則進入小對象申請流程,否則進入Tiny對象申請流程
-
Tiny對象申請流程后,會先獲取mcache目前的tinyoffset,再根據申請tiny對象的大小及mcache.tinyoffset值,進行內存對齊,計算出滿足內存對齊后的對象插入位置offset
-
如果從插入位置offset插入對象后,不超出16B,并且存在待分配的tiny空間,則將對象填充到該tiny空間,并將地址返回給M,結束內存申請
-
如果當前的tiny空間不足,則通過nextFreeFast(span)查找span中一個可用對象地址,存在則返回地址,并結束內存申請
-
如果span中不存在一個可用對象,則調用mcache.nextFree(tinySpanClass)從mcentral申請1個相同規格的msapn。申請成功則結束流程
3.2 小對象分配流程
-
進入小對象申請流程后,通過mcache.alloc(spc)獲取1個指定規格的mspan
-
通過nextFreeFast(span)查找span中一個可用對象地址,存在則返回地址給協程邏輯層P,P得到內存空間,流程結束
-
如果不存在可用對象,則通過mcache.nextFree(tinySpanClass)中mcache.refill(spc)從mcentral申請1個相同規格的msapn
4.mcache.refill(spc)中,會首先嘗試通過mcentral的noempty list獲取mspan,獲取不到則在嘗試通過mcentral的empty list獲取mspan(1.16之后,通過mcentral.cacheSpan()從partial set獲取mspan,獲取不到則從full set獲取可回收的mspan)。mcache成功獲取mcentral返回的mspan后,返回可用對象地址,結束申請流程
-
mcache中empty List(1.16之后,full set)也沒有可回收的mspan,則會調用mcache.grow()函數,從mheap中申請內存
-
mheap收到內存請求從其中一個heapArena從取出一部分pages返回給mcentral;當mheap沒有足夠的內存時,mheap會向操作系統申請內存,將申請的內存也保存到heapArena中的mspan中。mcentral將從mheap獲取的由Pages組成的mspan添加到對應的span class鏈表或集合中
-
最后協程業務邏輯層得到該對象申請到的內存,流程結束
3.3 大對象分配流程
-
進入大對象分配流程后,會調用mcache.allocLarge()方法申請大對象
-
mcache.allocLarge()中主要的mspan申請鏈路為:mheap.alloc -> mheap.allocSpan,mheap.allocSpan為申請mspan的核心方法。mheap.allocSpan會首先判斷申請的page數是否小于P.pageCache的最大page數,如果P.pageCache滿足需要,則會從P.mspancache獲取mspan地址給P,流程結束
-
P.pageCache不足,則對mheap加鎖,從mheap.pageAlloc這種Radix tree(基數樹)數據結構中查找可用的page,協程邏輯層P得到內存,流程結束
-
mheap.pageAlloc中查找不存在可用的page,則調用mheap.grow()向操作系統申請內存。申請成功后,再次從mheap.pageAlloc中查找可以page,P得到內存后,流程結束