圖解Go語言內存分配 - 知乎
go內置運行時,采用了自主管理,實現更好的內存使用模式,不需要每次內存分配都進行系統調用
采用TCMalloc
算法:把內存分為多級管理,從而降低鎖的粒度
將可用的堆內存采用二級分配的方式進行管理:
每個線程都會自行維護一個獨立的內存池,內存分配時優先從內存池中分配,內存池不足時向全局內存池申請,避免對全局內存池的頻繁競爭
在程序啟動時,向操作系統申請一塊內存
arena:堆區,go動態分配內存,把內存分割為8kb大小的頁,一些頁組合稱為mspan
bitmap:標識 arena 中哪些地址保存了對象,并用4bit標志位標識對象是否包含指針,GC標記信息
? 1個byte大小內存對應 arena區域中4個指針大小(指針大小8B)內存
bitmap的高地址指向arena的低地址,bitmap由高地址向低地址增長
span:存放mspan(arena 分割的頁組合起來的內存管理基本單元)的指針,每個指針對應一頁
? 創建mspan時,按頁填充對應的spans區域
內存管理單元
mspan:內存管理的基本單元,由一片連續的8kb的頁組成的大塊內存,一般是操作系統頁大小的幾倍,包含起始地址、mspan
規格、頁的數量等內容的雙端鏈表
每個mspan按照自身的屬性Size Class大小分割為若干個object,每個object存儲一個對象
并且使用一個位圖標記未使用的object
屬性Size Class決定object大小,mspan只會分配給object尺寸大小接近的對象,對象大小小于object
每個Size Class兩個mspan(span class)一個分配給含有指針的對象,一個分配個不含有指針對象
操作系統存儲模型
多級模型 動態切換(因為數據的調用頻率在實時發生變化)
虛擬內存與物理內存
在真實物理內存上的虛擬概念,用來代理,貼合用戶使用視角
使用戶(進程)覺得內存是連續的,而不是割裂的
實現內存“放大”的效果,虛擬內存可以由物理內存+磁盤補足,根據數據冷熱進行動態置換
通過頁表建立真實物理空間的映射關系
頁表:聚合映射關系的數據結構
分頁管理
虛擬內存中最小的操作單元:頁
物理內存:幀
提高內存空間利用率(以頁為粒度,外部碎片被替換為內部碎片,更相對可控)
提高內外交換效率
與虛擬內存機制呼應,建立虛擬地址到物理地址的映射關系
Golang內存模型
以空間換時間,一次緩存,多次復用
每次向操作系統多申請內存
堆mheap同樣的思想:
對操作系統,用戶進程中緩存的內存
對Go進程內部,堆是所有對象的內存起源
多級緩存,實現無/細鎖化
mcentral 根據對象的大小從小到大排列等級(集合)
當分配內存給一個對象時,會根據其大小找到從屬的等級,在對應的mcentral中嘗試獲取內存
mcache 每個處理器§單獨的本地私有緩存,其中冗余每一種等級的內存空間
當嘗試獲取內存時,根據處理器,查看本地私有的mcache中是否有合適的空間使用,有則直接獲取,無則訪問mcentral,一級一級升級向上獲取
多級規格,提高利用率
mcentral
page:go中有獨立的內存存儲單元,最小的存儲單元 8KB
mspan:最小的管理單元,為page的整數倍,從8B到80KB分為67種不同的規格,分配對象時根據大小映射到不同規格的mspan,獲取空間
隱藏的0級,處理更大的對象,上不封頂
mspan
屬于mcentral的一個節點,根據span的不同等級有一個central實例,central存儲的span個數是復數
stratAddr 起始地址 映射到Go語言中堆內存的一部分空間,也是虛擬內存中對應的地址
npages 頁的頁數,每個mspan中的頁是連續的,可以根據起始地址+頁數推導出,對應的內存區域
allocCache 基于bitmap輔助快速找到空閑內存塊(塊大小為對應等級下對象大小)
查找mspan中哪些頁是空閑的,可以把對象分配到里面
type mspan struct {_ sys.NotInHeap// 標識前后節點的指針next *mspan prev *mspan // 起始地址startAddr uintptr// 包含頁數,頁是連續的npages uintptr // 標識此前的位置(bit位)都已被占用freeindex uint16// 最多可存放多少個對象(會出現一頁存放多個對象的情況)nelems uint16// 每個bit對應一個對象塊,標識該塊是否被占用allocCache uint64// 標識mspan等級,包含class和noscan信息spanclass spanClass......
}
bytes/obj:該大小規格的對象會從當前等級的mspan中獲取空間,創建對象過程中,大小會向上取整為8B的整數倍,可以直接實現對象到mspan等級映射
bytes/span:該等級的mspan的總空間大小
object:最多可以new多少個對象
max waste:
// nocan 標識對象是否包含指針,在gc時是否需要展開標記
// 存在指針需要展開掃描形成完整的有向圖
// 會將兩類對象,劃分給不同的span(同一等級下的不同span)
// 將class+nocan組裝為uint8,形成完整的spanClass 標識
// 高7位表示span等級,最低為表示nocan信息
type spanClass uint8func 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
}
mcache
每個P獨有的緩存,交互無鎖
alloc 將每種spanClass等級的mspan各緩存一個,總數為2*68
對象分配器 tiny allocator 處理小于16B對象的內存分配
type mcache struct {_ sys.NotInHeap// 微對象分配器相關tiny uintptrtinyoffset uintptrtinyAllocs uintptr// mcache中緩存的mspan,每種各一個alloc [numSpanClasses]*mspan ......
}
mcentral中心緩存
每個central對應一個spanClass
會將mspan分為兩個鏈表,有空間mspan鏈表partial和滿空間mspan鏈表 full
type mcentral struct {_ sys.NotInHeap// 對應的spanClassspanclass spanClass// 有空位mspan集合// 數組長度為2,抗一輪GCpartial [2]spanSet // 無空位full [2]spanSet
}
mheap全局堆緩存
對于Golang上層應用,堆是操作系統虛擬內存的抽象
堆內存中最小內存存儲單元:頁(8KB)
負責將連續頁組裝為mspan
全局內存基于bitMap標識使用情況,一個bit對應一頁,0自由,1已被組裝為mspan
通過heapArena聚合頁,記錄頁到mspan的映射信息,維護頁所屬的mspan關系
建立空閑頁基數樹索引 radix tree index 快速尋找空閑頁,獲取至少在虛擬視角下連續的空閑頁
mcentral 的持有者,持有所有spanClass下mcentral,作為自身緩存,mcentral是mheap更細粒度的緩存(以空間換時間的操作)
內存不夠,向操作系統申請,單位:heapArena(64M)
type mheap struct {_ sys.NotInHeap// 堆的全局鎖(進程維度)lock mutex// 空閑頁分配器,底層由多棵基數樹組成的索引,每棵樹對應16GB內存空間pages pageAlloc // 記錄所有mspan// 所有mspan都是由mheap,使用連續空閑頁組裝allspans []*mspan // heapAreana 數組 請求內存和映射關系// 64位系統下,二維數組容量為[1][2^22]// 每個heapArena大小64M// 理論上堆的上限為 2^22*64M = 256Tarenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena// 多個mcentral,總數為spanClass 個數central [numSpanClasses]struct {mcentral mcentral// 用于內存地址對齊pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte}......
}