Golang 內存模型與分配機制

0 前言

未來兩週,想和大家探討的主題是 Golang 內存管理機制.

本系列會分爲兩篇,第一篇談及 Golang 內存模型以及內存分配機制,第二篇會和大家討論 Golang 的垃圾回收機制. 本文是其中第一篇.

我個人比較推崇” 基於源碼支撐原理 “的信念,所以本文在闡述原理的基礎上,會伴有大量源碼走讀的過程,作爲理論的支撐論證. 走讀的 Go 源碼版本爲 1.19.

內存管理與垃圾回收都屬 Go 語言最複雜的模塊,受限於筆者個人水平,文章內容可能有不足或紕漏之處,很歡迎大家添加我的個人 VX:xingyun19951212 進行批評指正.

1 內存模型

1.1 操作系統存儲模型

本文既然要聊到 Golang 的內存模型設計,就讓我們首先回顧操作系統中經典的多級存儲模型設計.

觀察上圖,我們可以從中捕捉到的關鍵詞是:

1.2 虛擬內存與物理內存

操作系統內存管理中,另一個重要概念是虛擬內存,其作用如下:

1.3 分頁管理

操作系統中通常會將虛擬內存和物理內存切割成固定的尺寸,於虛擬內存而言叫作 “頁”,於物理內存而言叫作 “幀”,原因及要點如下:

1.4 Golang 內存模型

前幾小節的鋪墊,旨在從 “內存模型設計” 這件事情中收穫一些觸類旁通的設計理念.

下面步入正題,聊聊 Golang 的內存模型設計的幾個核心要點:

由於每次向操作系統申請內存的操作很重,那麼不妨一次多申請一些,以備後用.

Golang 中的堆 mheap 正是基於該思想,產生的數據結構. 我們可以從兩個視角來解決 Golang 運行時的堆:

I 對操作系統而言,這是用戶進程中緩存的內存

II 對於 Go 進程內部,堆是所有對象的內存起源

堆是 Go 運行時中最大的臨界共享資源,這意味着每次存取都要加鎖,在性能層面是一件很可怕的事情.

在解決這個問題,Golang 在堆 mheap 之上,依次細化粒度,建立了 mcentral、mcache 的模型,下面對三者作個梳理:

這些概念,我們在第 2 節中都會再作詳細展開,此處可以先不深究,注重於宏觀架構即可.

首先理下 page 和 mspan 兩個概念:

(1)page:最小的存儲單元.

Golang 借鑑操作系統分頁管理的思想,每個最小的存儲單元也稱之爲頁 page,但大小爲 8 KB

(2)mspan:最小的管理單元.

mspan 大小爲 page 的整數倍,且從 8B 到 80 KB 被劃分爲 67 種不同的規格,分配對象時,會根據大小映射到不同規格的 mspan,從中獲取空間.

於是,我們回頭小節多規格 mspan 下產生的特點:

I 根據規格大小,產生了等級的制度

II 消除了外部碎片,但不可避免會有內部碎片

III 宏觀上能提高整體空間利用率

IV 正是因爲有了規格等級的概念,才支持 mcentral 實現細鎖化

上圖是 Thread-Caching Malloc 的整體架構圖,Golang 正是借鑑了該內存模型. 我們先看眼架構,有個整體概念,後續小節中,我們會不斷對細節進行補充.

2 核心概念梳理

2.1 內存單元 mspan

分點闡述 mspan 的特質:

mspan 類的源碼位於 runtime/mheap.go 文件中:

type mspan struct {
    // 標識前後節點的指針 
    next *mspan     
    prev *mspan    
    // ...
    // 起始地址
    startAddr uintptr 
    // 包含幾頁,頁是連續的
    npages    uintptr 


    // 標識此前的位置都已被佔用 
    freeindex uintptr
    // 最多可以存放多少個 object
    nelems uintptr // number of object in the span.


    // bitmap 每個 bit 對應一個 object 塊,標識該塊是否已被佔用
    allocCache uint64
    // ...
    // 標識 mspan 等級,包含 class 和 noscan 兩部分信息
    spanclass             spanClass    
    // ...
}

2.2 內存單元等級 spanClass

mspan 根據空間大小和麪向分配對象的大小,被劃分爲 67 種等級(1-67,實際上還有一種隱藏的 0 級,用於處理更大的對象,上不封頂)

下表展示了部分的 mspan 等級列表,數據取自 runtime/sizeclasses.go 文件中:

u7A3uv

對上表各列進行解釋:

(1)class:mspan 等級標識,1-67

(2)bytes/obj:該大小規格的對象會從這一 mspan 中獲取空間. 創建對象過程中,大小會向上取整爲 8B 的整數倍,因此該表可以直接實現 object 到 mspan 等級 的映射

(3)bytes/span:該等級的 mspan 的總空間大小

(4)object:該等級的 mspan 最多可以 new 多少個對象,結果等於 (3)/(2)

(5)tail waste:(3)/(2)可能除不盡,於是該項值爲(3)%(2)

(6)max waste:通過下面示例解釋:

以 class 3 的 mspan 爲例,class 分配的 object 大小統一爲 24B,由於 object 大小 <= 16B 的會被分配到 class 2 及之前的 class 中,因此只有 17B-24B 大小的 object 會被分配到 class 3.

最不利的情況是,當 object 大小爲 17B,會產生浪費空間比例如下:

    ((24-17)*341 + 8)/8192 = 0.292358 ≈ 29.24%

除了上面談及的根據大小確定的 mspan 等級外,每個 object 還有一個重要的屬性叫做 nocan,標識了 object 是否包含指針,在 gc 時是否需要展開標記.

在 Golang 中,會將 span class + nocan 兩部分信息組裝成一個 uint8,形成完整的 spanClass 標識. 8 個 bit 中,高 7 位表示了上表的 span 等級(總共 67 + 1 個等級,8 個 bit 足夠用了),最低位表示 nocan 信息.

代碼位於 runtime/mheap.go

type spanClass uint8


// uint8 左 7 位爲 mspan 等級,最右一位標識是否爲 noscan
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
    return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}


func (sc spanClass) sizeclass() int8 {
    return int8(sc >> 1)
}


func (sc spanClass) noscan() bool {
    return sc&1 != 0
}

2.3 線程緩存 mcache

要點:

(1)mcache 是每個 P 獨有的緩存,因此交互無鎖

(2)mcache 將每種 spanClass 等級的 mspan 各緩存了一個,總數爲 2(nocan 維度) * 68(大小維度)= 136

(3)mcache 中還有一個爲對象分配器 tiny allocator,用於處理小於 16B 對象的內存分配,在 3.3 小節中詳細展開.

代碼位於 runtime/mcache.go:

const numSpanClasses = 136
type mcache struct {
    // 微對象分配器相關
    tiny       uintptr
    tinyoffset uintptr
    tinyAllocs uintptr
    
    // mcache 中緩存的 mspan,每種 spanClass 各一個
    alloc [numSpanClasses]*mspan 
    // ...
}

2.4 中心緩存 mcentral

要點:

(1)每個 mcentral 對應一種 spanClass

(2)每個 mcentral 下聚合了該 spanClass 下的 mspan

(3)mcentral 下的 mspan 分爲兩個鏈表,分別爲有空間 mspan 鏈表 partial 和滿空間 mspan 鏈表 full

(4)每個 mcentral 一把鎖

代碼位於 runtime/mcentral.go

type mcentral struct {
    // 對應的 spanClass
    spanclass spanClass
    // 有空位的 mspan 集合,數組長度爲 2 是用於抗一輪 GC
    partial [2]spanSet 
    // 無空位的 mspan 集合
    full    [2]spanSet 
}

2.5 全局堆緩存 mheap

要點:

代碼位於 runtime/mheap.go

type mheap struct {
    // 堆的全局鎖
    lock mutex


    // 空閒頁分配器,底層是多棵基數樹組成的索引,每棵樹對應 16 GB 內存空間
    pages pageAlloc 


    // 記錄了所有的 mspan. 需要知道,所有 mspan 都是經由 mheap,使用連續空閒頁組裝生成的
    allspans []*mspan


    // heapAreana 數組,64 位系統下,二維數組容量爲 [1][2^22]
    // 每個 heapArena 大小 64M,因此理論上,Golang 堆上限爲 2^22*64M = 256T
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena


    // ...
    // 多個 mcentral,總個數爲 spanClass 的個數
    central [numSpanClasses]struct {
        mcentral mcentral
        // 用於內存地址對齊
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }


    // ...
}

2.6 空閒頁索引 pageAlloc

與 mheap 中,與空閒頁尋址分配的基數樹索引有關的內容較爲晦澀難懂. 網上能把這個問題真正講清楚的文章幾乎沒有.

所幸我最後找到這個數據結構的作者發佈的筆記,終於對方案的原貌有了大概的瞭解,這裏粘貼鏈接,供大家自取:https://go.googlesource.com/proposal/+/master/design/35112-scaling-the-page-allocator.md

要理清這棵技術樹,首先需要明白以下幾點:

(1)數據結構背後的含義:

I 2.5 小節有提及,mheap 會基於 bitMap 標識內存中各頁的使用情況,bit 位爲 0 代表該頁是空閒的,爲 1 代表該頁已被 mspan 佔用.

II 每棵基數樹聚合了 16 GB 內存空間中各頁使用情況的索引信息,用於幫助 mheap 快速找到指定長度的連續空閒頁的所在位置

III mheap 持有 2^14 棵基數樹,因此索引全面覆蓋到 2^14 * 16 GB = 256 T 的內存空間.

(2)基數樹節點設定

基數樹中,每個節點稱之爲 PallocSum,是一個 uint64 類型,體現了索引的聚合信息,包含以下四部分:

(3)父子關係

代碼位於 runtime/mpagealloc.go

const summaryLevels = 5


type pageAlloc struct {
    // 共有五層基數樹,第一層有 2^14 個節點,因此共用 2^14棵基數樹
    // 總空間大小爲 2^14*16GB = 256T
    // 接下來每層的節點數爲上層的 8 倍
    summary [summaryLevels][]pallocSum
    
    // ...
    // 類似於 tiny offset,小於此值的地址無鎖檢索,必然沒有空間可用
    searchAddr offAddr


    // ...
}

基數樹節點

const(
    logMaxPackedValue = 21
    maxPackedValue    = 1 << logMaxPackedValue
)


type pallocSum uint64


// 基於 start、max、end 組裝成一個基數樹節點 pallocSum
func packPallocSum(start, max, end uint) pallocSum {
    // ...
    return pallocSum((uint64(start) & (maxPackedValue - 1)) |
        ((uint64(max) & (maxPackedValue - 1)) << logMaxPackedValue) |
        ((uint64(end) & (maxPackedValue - 1)) << (2 * logMaxPackedValue)))
}


// 當前節點對應區域內,首部連續空閒頁的長度
// 通過 uint64 最右側 21 個 bit 標識
func (p pallocSum) start() uint {
    // ...
    return uint(uint64(p) & (maxPackedValue - 1))
}


// 當前節點對應區域內,連續空閒頁的最大長度
// 通過 uint64 左數 23~43 個 bit 標識
func (p pallocSum) max() uint {
    // ...
    return uint((uint64(p) >> logMaxPackedValue) & (maxPackedValue - 1))
}


// 當前節點對應區域內,尾部連續空閒頁的長度
// 通過 uint64 左數 2~22 個 bit 標識
func (p pallocSum) end() uint {
    return uint((uint64(p) >> (2 * logMaxPackedValue)) & (maxPackedValue - 1))
}

2.7 heapArena

代碼位於 runtime/mheap.go

const pagesPerArena = 8192


type heapArena struct {
    // ...
    // 實現 page 到 mspan 的映射
    spans [pagesPerArena]*mspan


    // ...
}

3 對象分配流程

下面來串聯 Golang 中分配對象的流程,不論是以下哪種方式,最終都會殊途同歸步入 mallocgc 方法中,並且根據 3.1 小節中的策略執行分配流程:

3.1 分配流程總覽

Golang 中,依據 object 的大小,會將其分爲下述三類:

不同類型的對象,會有着不同的分配策略,這些內容在 mallocgc 方法中都有體現.

核心流程類似於讀多級緩存的過程,由上而下,每一步只要成功則直接返回. 若失敗,則由下層方法兜底.

對於微對象的分配流程:

(1)從 P 專屬 mcache 的 tiny 分配器取內存(無鎖)

(2)根據所屬的 spanClass,從 P 專屬 mcache 緩存的 mspan 中取內存(無鎖)

(3)根據所屬的 spanClass 從對應的 mcentral 中取 mspan 填充到 mcache,然後從 mspan 中取內存(spanClass 粒度鎖)

(4)根據所屬的 spanClass,從 mheap 的頁分配器 pageAlloc 取得足夠數量空閒頁組裝成 mspan 填充到 mcache,然後從 mspan 中取內存(全局鎖)

(5)mheap 向操作系統申請內存,更新頁分配器的索引信息,然後重複(4).

對於小對象的分配流程是跳過(1)步,執行上述流程的(2)-(5)步;

對於大對象的分配流程是跳過(1)-(3)步,執行上述流程的(4)-(5)步.

3.2 主幹方法 mallocgc

先上道硬菜,malloc 方法主幹全流程展示.

如果覺得理解曲線太陡峭,可以先跳到後續小節,把拆解的各部分模塊都熟悉後,再回過頭來總覽一遍.

代碼位於 runtime/malloc.go 文件中:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // ...    
    // 獲取 m
    mp := acquirem()
    // 獲取當前 p 對應的 mcache
    c := getMCache(mp)
    var span *mspan
    var x unsafe.Pointer
    // 根據當前對象是否包含指針,標識 gc 時是否需要展開掃描
    noscan := typ == nil || typ.ptrdata == 0
    // 是否是小於 32KB 的微、小對象
    if size <= maxSmallSize {
    // 小於 16 B 且無指針,則視爲微對象
        if noscan && size < maxTinySize {
        // tiny 內存塊中,從 offset 往後有空閒位置
          off := c.tinyoffset
          // 如果大小爲 5 ~ 8 B,size 會被調整爲 8 B,此時 8 & 7 == 0,會走進此分支
          if size&7 == 0 {
                // 將 offset 補齊到 8 B 倍數的位置
                off = alignUp(off, 8)
                // 如果大小爲 3 ~ 4 B,size 會被調整爲 4 B,此時 4 & 3 == 0,會走進此分支  
           } else if size&3 == 0 {
           // 將 offset 補齊到 4 B 倍數的位置
                off = alignUp(off, 4)
                // 如果大小爲 1 ~ 2 B,size 會被調整爲 2 B,此時 2 & 1 == 0,會走進此分支  
           } else if size&1 == 0 {
            // 將 offset 補齊到 2 B 倍數的位置
                off = alignUp(off, 2)
           }
// 如果當前 tiny 內存塊空間還夠用,則直接分配並返回
            if off+size <= maxTinySize && c.tiny != 0 {
            // 分配空間
                x = unsafe.Pointer(c.tiny + off)
                c.tinyoffset = off + size
                c.tinyAllocs++
                mp.mallocing = 0
                releasem(mp)  
                return x
            } 
            // 分配一個新的 tiny 內存塊
            span = c.alloc[tinySpanClass]    
            // 從 mCache 中獲取
            v := nextFreeFast(span)        
            if v == 0 {
            // 從 mCache 中獲取失敗,則從 mCentral 或者 mHeap 中獲取進行兜底
                v, span, shouldhelpgc = c.nextFree(tinySpanClass)
            }   
// 分配空間      
            x = unsafe.Pointer(v)
           (*[2]uint64)(x)[0] = 0
           (*[2]uint64)(x)[1] = 0
           size = maxTinySize
        } else {
          // 根據對象大小,映射到其所屬的 span 的等級(0~66)
          var sizeclass uint8
          if size <= smallSizeMax-8 {
              sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
          } else {
              sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
          }        
          // 對應 span 等級下,分配給每個對象的空間大小(0~32KB)
          size = uintptr(class_to_size[sizeclass])
          // 創建 spanClass 標識,其中前 7 位對應爲 span 的等級(0~66),最後標識表示了這個對象 gc 時是否需要掃描
          spc := makeSpanClass(sizeclass, noscan) 
          // 獲取 mcache 中的 span
          span = c.alloc[spc]  
          // 從 mcache 的 span 中嘗試獲取空間        
          v := nextFreeFast(span)
          if v == 0 {
          // mcache 分配空間失敗,則通過 mcentral、mheap 兜底            
             v, span, shouldhelpgc = c.nextFree(spc)
          }     
          // 分配空間  
          x = unsafe.Pointer(v)
          // ...
       }      
       // 大於 32KB 的大對象      
   } else {
       // 從 mheap 中獲取 0 號 span
       span = c.allocLarge(size, noscan)
       span.freeindex = 1
       span.allocCount = 1
       size = span.elemsize         
       // 分配空間   
        x = unsafe.Pointer(span.base())
   }  
   // ...
   return x
}

3.3 步驟(1):tiny 分配

每個 P 獨有的 mache 會有個微對象分配器,基於 offset 線性移動的方式對微對象進行分配,每 16B 成塊,對象依據其大小,會向上取整爲 2 的整數次冪進行空間補齊,然後進入分配流程.

    noscan := typ == nil || typ.ptrdata == 0
    // ...
        if noscan && size < maxTinySize {
        // tiny 內存塊中,從 offset 往後有空閒位置
          off := c.tinyoffset
          // ...
            // 如果當前 tiny 內存塊空間還夠用,則直接分配並返回
            if off+size <= maxTinySize && c.tiny != 0 {
            // 分配空間
                x = unsafe.Pointer(c.tiny + off)
                c.tinyoffset = off + size
                c.tinyAllocs++
                mp.mallocing = 0
                releasem(mp)
                return x
            }
           // ...
        }

3.4 步驟(2):mcache 分配

          // 根據對象大小,映射到其所屬的 span 的等級(0~66)
          var sizeclass uint8
          // get size class ....     
          // 對應 span 等級下,分配給每個對象的空間大小(0~32KB)
          // get span class
          spc := makeSpanClass(sizeclass, noscan) 
          // 獲取 mcache 中的 span
          span = c.alloc[spc]  
          // 從 mcache 的 span 中嘗試獲取空間        
          v := nextFreeFast(span)
          if v == 0 {
          // mcache 分配空間失敗,則通過 mcentral、mheap 兜底            
             v, span, shouldhelpgc = c.nextFree(spc)
          }     
          // 分配空間  
          x = unsafe.Pointer(v)

在 mspan 中,基於 Ctz64 算法,根據 mspan.allocCache 的 bitMap 信息快速檢索到空閒的 object 塊,進行返回.

代碼位於 runtime/malloc.go 文件中:

func nextFreeFast(s *mspan) gclinkptr {
    // 通過 ctz64 算法,在 bit map 上尋找到首個 object 空位
    theBit := sys.Ctz64(s.allocCache) 
    if theBit < 64 {
        result := s.freeindex + uintptr(theBit)
        if result < s.nelems {
            freeidx := result + 1
            if freeidx%64 == 0 && freeidx != s.nelems {
                return 0
            }
            s.allocCache >>= uint(theBit + 1)
            // 偏移 freeindex 
            s.freeindex = freeidx
            s.allocCount++
            // 返回獲取 object 空位的內存地址 
            return gclinkptr(result*s.elemsize + s.base())
        }
    }
    return 0
}

3.5 步驟(3):mcentral 分配

當 mspan 無可用的 object 內存塊時,會步入 mcache.nextFree 方法進行兜底.

代碼位於 runtime/mcache.go 文件中:

func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
    s = c.alloc[spc]
    // ...
    // 從 mcache 的 span 中獲取 object 空位的偏移量
    freeIndex := s.nextFreeIndex()
    if freeIndex == s.nelems {
        // ...
        // 倘若 mcache 中 span 已經沒有空位,則調用 refill 方法從 mcentral 或者 mheap 中獲取新的 span    
        c.refill(spc)
        // ...
        // 再次從替換後的 span 中獲取 object 空位的偏移量
        s = c.alloc[spc]
        freeIndex = s.nextFreeIndex()
    }
    // ...
    v = gclinkptr(freeIndex*s.elemsize + s.base())
    s.allocCount++
    // ...
    return
}

倘若 mcache 中,對應的 mspan 空間不足,則會在 mcache.refill 方法中,向更上層的 mcentral 乃至 mheap 獲取 mspan,填充到 mache 中:

代碼位於 runtime/mcache.go 文件中:

func (c *mcache) refill(spc spanClass) {  
    s := c.alloc[spc]
    // ...
    // 從 mcentral 當中獲取對應等級的 span
    s = mheap_.central[spc].mcentral.cacheSpan()
    // ...
    // 將新的 span 添加到 mcahe 當中
    c.alloc[spc] = s
}

mcentral.cacheSpan 方法中,會加鎖(spanClass 級別的 sweepLocker),分別從 partial 和 full 中嘗試獲取有空間的 mspan:

代碼位於 runtime/mcentral.go 文件中:

func (c *mcentral) cacheSpan() *mspan {
    // ...
    var sl sweepLocker    
    // ...
    sl = sweep.active.begin()
    if sl.valid {
        for ; spanBudget >= 0; spanBudget-- {
            s = c.partialUnswept(sg).pop()
            // ...
            if s, ok := sl.tryAcquire(s); ok {
                // ...
                sweep.active.end(sl)
                goto havespan
            }
            
        // 通過 sweepLock,加鎖嘗試從 mcentral 的非空鏈表 full 中獲取 mspan
        for ; spanBudget >= 0; spanBudget-- {
            s = c.fullUnswept(sg).pop()
           // ...
            if s, ok := sl.tryAcquire(s); ok {
                // ...
                sweep.active.end(sl)
                goto havespan
                }
                // ...
            }
        }
        // ...
    }
    // ...


    // 執行到此處時,s 已經指向一個存在 object 空位的 mspan 了
havespan:
    // ...
    return
}

3.6 步驟(4):mheap 分配

在 mcentral.cacheSpan 方法中,倘若從 partial 和 full 中都找不到合適的 mspan 了,則會調用 mcentral 的 grow 方法,將事態繼續升級:

func (c *mcentral) cacheSpan() *mspan {
    // ...
    // mcentral 中也沒有可用的 mspan 了,則需要從 mheap 中獲取,最終會調用 mheap_.alloc 方法
    s = c.grow()
   // ...


    // 執行到此處時,s 已經指向一個存在 object 空位的 mspan 了
havespan:
    // ...
    return
}

經由 mcentral.grow 方法和 mheap.alloc 方法的週轉,最終會步入 mheap.allocSpan 方法中:

func (c *mcentral) grow() *mspan {
    npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
    size := uintptr(class_to_size[c.spanclass.sizeclass()])


    s := mheap_.alloc(npages, c.spanclass)
    // ...


    // ...
    return s
}

代碼位於 runtime/mheap.go

func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
    var s *mspan
    systemstack(func() {
        // ...
        s = h.allocSpan(npages, spanAllocHeap, spanclass)
    })
    return s
}

代碼位於 runtime/mheap.go

func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) {
    gp := getg()
    base, scav := uintptr(0), uintptr(0)
    
    // ...此處實際上還有一階緩存,是從每個 P 的頁緩存 pageCache 中獲取空閒頁組裝 mspan,此處先略去了...
    
    // 加上堆全局鎖
    lock(&h.lock)
    if base == 0 {
        // 通過基數樹索引快速尋找滿足條件的連續空閒頁
        base, scav = h.pages.alloc(npages)
        // ...
    }
    
    // ...
    unlock(&h.lock)


HaveSpan:
    // 把空閒頁組裝成 mspan
    s.init(base, npages)
    
    // 將這批頁添加到 heapArena 中,建立由頁指向 mspan 的映射
    h.setSpans(s.base(), npages, s)
    // ...
    return s
}

倘若對 mheap 空閒頁分配器基數樹 pageAlloc 分配空閒頁的源碼感興趣,莫慌,3.8 小節見.

3.7 步驟(5):向操作系統申請

倘若 mheap 中沒有足夠多的空閒頁了,會發起 mmap 系統調用,向操作系統申請額外的內存空間.

代碼位於 runtime/mheap.go 文件的 mheap.grow 方法中:

func (h *mheap) grow(npage uintptr) (uintptr, bool) {
    av, asize := h.sysAlloc(ask)
}
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
       v = sysReserve(unsafe.Pointer(p), n)
}
func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer {
    return sysReserveOS(v, n)
}
func sysReserveOS(v unsafe.Pointer, n uintptr) unsafe.Pointer {
    p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)
    if err != 0 {
        return nil
    }
    return p
}

3.8 步驟(4)拓展:基數樹尋頁

核心源碼位於 runtime/pagealloc.go 的 pageAlloc 方法中,要點都以在代碼中給出註釋:

func (p *pageAlloc) find(npages uintptr) (uintptr, offAddr) {
    // 必須持有堆鎖
    assertLockHeld(p.mheapLock)


    // current level.
    i := 0


    // ...
    lastSum := packPallocSum(0, 0, 0)
    lastSumIdx := -1


nextLevel:
    // 1 ~ 5 層依次遍歷
    for l := 0; l < len(p.summary); l++ {
        // ...
        // 根據上一層的 index,映射到下一層的 index.
        // 映射關係示例:上層 0 -> 下層 [0~7]
        //             上層 1 -> 下層 [8~15]
        //             以此類推
        i <<= levelBits[l]
        entries := p.summary[l][i : i+entriesPerBlock]
        // ...
        // var levelBits = [summaryLevels]uint{
        //   14,3,3,3,3
        // }
        // 除第一層有 2^14 個節點外,接下來每層都只要關心 8 個 節點.
        // 由於第一層有 2^14 個節點,所以 heap 內存上限爲 2^14 * 16G = 256T
        var base, size uint
        for j := j0; j < len(entries); j++ {
            sum := entries[j]
            // ...
            // 倘若當前節點對應內存空間首部即滿足,直接返回結果
            s := sum.start()
            if size+s >= uint(npages) {               
                if size == 0 {
                    base = uint(j) << logMaxPages
                }             
                size += s
                break
            }
            // 倘若當前節點對應內存空間首部不滿足,但是內部最長連續頁滿足,則到下一層節點展開搜索
            if sum.max() >= uint(npages) {               
                i += j
                lastSumIdx = i
                lastSum = sum
                continue nextLevel
            }
            // 即便內部最長連續頁不滿足,還可以嘗試將尾部與下個節點的首部疊加,看是否滿足
            if size == 0 || s < 1<<logMaxPages {
                                size = sum.end()
                base = uint(j+1)<<logMaxPages - size
                continue
            }
            // The entry is completely free, so continue the run.
            size += 1 << logMaxPages
        }
    
    // 根據 i 和 j 可以推導得到對應的內存地址,進行返回
    ci := chunkIdx(i)
    addr := chunkBase(ci) + uintptr(j)*pageSize
    // ...
    return addr, p.findMappedAddr(firstFree.base)
}

4 展望

祝賀,到此爲止,整個 Golang 內存分配流程已經梳理完畢.

兩週內,我會帶來新作——Golang 垃圾回收機制.

Go 友們不見不散~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2TBwpQT5-zU4Gy7-i0LZmQ