【Go萬字洗髓經】Golang內存模型與內存分配管理

在這里插入圖片描述

本文目錄

  • 1. 操作系統中的虛擬內存
    • 分頁與進程管理
    • 虛擬內存與內存隔離
  • 2. Golang中的內存模型
    • 內存分配流程
    • 內存單元mspan
    • 線程緩存mcache
    • 中心緩存mcentral
    • 全局堆緩存mheap
    • heapArena
    • 空閑頁索引pageAlloc
  • 3. Go對象分配
    • mallocgc函數
    • tiny對象分配內存
  • 4.結合GMP模型來看內存模型
    • tiny對象分配
  • 5.總結
    • 設計思想
    • 一些問題?
      • 為什么mcache與P綁定?
      • span的等級到底是66級還是67級或者68級?
      • 0級到底是什么?是更大對象嗎?
  • 6. 參考文章

1. 操作系統中的虛擬內存

虛擬內存是操作系統中一種重要的內存管理技術。它允許計算機系統使用硬盤空間來模擬額外的內存空間,從而擴展可用內存的范圍。

從用戶程序的角度來看,虛擬內存提供了一個比實際物理內存大得多的地址空間。操作系統通過將程序和數據分段存儲在硬盤上的虛擬內存區域,并在需要時動態地將部分數據加載到物理內存中來實現這一功能。

這種方式使得大型程序能夠在有限的物理內存環境中運行,同時也提高了內存的利用率。例如,當物理內存不足時,操作系統會將暫時不用的數據或程序代碼移動到虛擬內存中,而當這些數據被訪問時,再將其調回到物理內存。

虛擬內存的管理涉及到頁面置換算法段頁式管理等多種技術,這些技術共同確保了虛擬內存的有效運行,并為用戶提供了一個高效且透明的內存使用環境。

比如下面這個圖,是一個簡單的示意。虛擬內存可以通過頁表來定位到真實數據到底位于哪里,從而進行訪問數據。

在這里插入圖片描述
虛擬內存是以“頁”進行單位進行管理,物理內存是“幀”。

操作系統中虛擬內存和物理內存被切割成固定尺寸的“頁”和“幀”有其特定的意義和好處。首先,這樣做可以提高內存空間的利用效率。當內存以頁為粒度進行管理時,可以消除不穩定的外部碎片,取而代之的是相對可控的內部碎片。這意味著內存的使用更加高效,減少了浪費。(內部的碎片是指頁內的碎片地址,比如說4k,只用了3k,所以多的1k是多余的。

其次,將內存分割成頁和幀可以提高內存與外部存儲之間的交換效率。更細的粒度意味著更高的靈活性,操作系統可以更靈活地管理內存,從而提高內外存交換的效率。

此外,這種分割方式與虛擬內存機制相呼應,便于建立虛擬地址到物理地址的映射關系。這種映射關系是通過一種稱為頁表的數據結構來實現的,它聚合了映射關系,使得虛擬內存的管理和訪問更加高效。

在Linux系統中,頁或幀的大小是固定的,通常為4KB。這個大小實際上是由實踐經驗決定的。如果頁或幀太大,會增加內存碎片率,導致內存利用不充分;如果太小,則會增加分配頻率,影響系統效率。因此,4KB是一個平衡點,既能保證內存的有效利用,又能保持較高的系統效率。

所以總的來說,分頁不僅是為了防止外部的內存碎片導致的內存浪費,也還是因為多進程時代內存可能溢出的問題,主要還是為了做進程管理。

另外就是虛擬內存不是只讓用戶看著空間更大,主要是為了解決內存隔離的問題。

這里簡單提一下進程管理和內存隔離,不是本文重點。

分頁與進程管理

進程管理是操作系統的一項基本功能,它涉及到進程的創建、調度、執行和終止。進程是操作系統進行資源分配和調度的基本單位。每個進程都有自己的地址空間,操作系統通過進程管理確保每個進程都能安全、有效地使用系統資源,并且與其他進程隔離開來。

內存泄露(Memory Leak)是指程序在申請內存后,未能在不需要時正確釋放,導致隨著時間的推移,大量內存無法被回收利用,最終可能導致程序或系統性能下降,甚至崩潰。內存泄露是編程中常見的問題,特別是在那些需要頻繁動態分配內存的程序中。

分頁是現代操作系統中常用的內存管理技術之一。通過將內存分割成固定大小的頁(Page),操作系統可以更有效地管理內存,同時也為進程管理提供了便利。每個進程都有自己的一組頁,這些頁映射到物理內存中。這種隔離機制可以防止一個進程訪問或修改另一個進程的內存,從而提高了系統的穩定性和安全性。分頁機制不僅有助于防止惡意軟件隨意訪問內存,也有助于防止進程因內存溢出而相互干擾。

虛擬內存與內存隔離

操作系統為每個進程提供了一個獨立的虛擬地址空間,進程通過虛擬地址訪問內存。操作系統使用頁表將虛擬地址映射到物理內存地址,這個過程對進程是透明的。由于每個進程都有其獨立的頁表,因此它們無法訪問其他進程的虛擬內存空間。

分頁是實現內存隔離的一種技術,操作系統將內存分割成固定大小的頁(Page),每個進程只能訪問分配給它的頁。如果進程嘗試訪問未分配給它的頁,操作系統會阻止這種訪問,從而防止進程之間的內存干擾。

2. Golang中的內存模型

有一個很核心的點是,以空間換時間,一次緩存,多次復用

因為每次申請內存的代價比較大,所以可以多申請一些內存,方便后續程序不斷地使用。如果長時間申請的內存都是閑置的,那么就可以歸還給操作系統。

內存分配流程

從操作系統的角度來看,這是用戶進程(golang程序)中緩存的內存

從Go自己的角度來看,是所有對象的內存起源,所有的對象的內存都是“堆”申請到的內存。

為了提高分配內存的效率,Go還設計了多級緩存,從而實現無鎖化、細鎖化粒度

我們可以看看下面這個邏輯分層圖,注意,是邏輯分層圖。只是為了最開始方便理解內存模型,后邊隨著深入講解,會不斷地延伸。

在這里插入圖片描述

mheap是全局唯一的,如果要和mheap進行操作申請內存,需要加一個全局鎖。因為堆是全局唯一的,所以這個鎖也是全局鎖,和進程(Go程序)一對一的。

mheap上細化粒度,建立了有mcentral可以理解為一個等級集合的概念,根據最終需要創建的對象的大小區別,排了一個等級,當想要分配某個內存給某個對象實例的時候,會判斷這個實例的大小,然后分配對應的mcentral。這樣就把鎖的粒度細化到了mcentral。也就是同一個大小等級內的所有對象,去競爭這個鎖,優化了性能。

mcache就是GMP調度器中的處理器Processmcache就是每一個處理器P獨一份的、本地私有的緩存mcachemcache中會冗余每一種等級的空間mspan,也就是會為每一個處理器去冗余一個內存空間。

當去獲取內存的時候,先根據這個Process去查看其本地私有的mcache中有沒有適合的內存空間使用,如果有,就直接獲取使用,因為是 中私有的,所以不涉及并發,是無鎖的,這是最理想的情況下。

如果說mcache沒有空間,沒有辦法通過無鎖的形式進行獲取內存的行為,就會把這個行為升級,去mcentral中想辦法分配內存。如果還是不行,就會繼續升級,就回去mheap中獲取內存空間。如果還是不行,就會發起系統調用的指令,去虛擬內存中申請更多的空間給mheap,然后再給我們的這次行為分配內存使用。

所以大概可以梳理個流程了。假設我們正在運行一個Go協程,該程序需要創建一個大小為128字節的對象。以下是分配這個對象可能經歷的步驟:

  • 本地緩存查找(mcache)

程序首先檢查它所屬的處理器P的本地私有緩存mcache中是否有足夠大的內存空間mspan來存放這個128字節的對象。mcache中為每種大小的對象都準備了冗余的內存空間,以減少鎖的競爭和提高效率。

如果mcache中有合適大小的mspan,那么程序將直接從mcache中分配內存,這個過程是無鎖的,因為每個處理器都有自己的mcache,不涉及并發問題。

  • 中央緩存查找(mcentral)

如果mcache中沒有合適大小的mspan,程序將嘗試從mcentral中獲取。mcentral是一個按對象大小分類的中央緩存,它管理著相同大小對象的內存分配。

mcentral中,程序會找到管理128字節對象的mcentral實例。由于mcentral是按大小分類的,所以這里的鎖競爭僅限于相同大小的對象,這進一步細化了鎖的粒度,提高了性能。

如果mcentral中有空閑的mspan,它將被分配給程序。如果沒有,mcentral將嘗試從mheap中獲取新的內存空間。

  • 堆內存分配(mheap)

如果mcentral也無法提供內存,那么程序將直接向mheap申請內存。mheap是Go程序的全局堆內存,負責管理整個程序的內存分配。

由于mheap是全局唯一的,操作mheap需要加一個全局鎖,以確保內存分配的原子性和一致性。這是整個內存分配過程中鎖粒度最大的一步,但因為前面的步驟已經盡可能地減少了對mheap的直接操作,所以這種情況相對較少。

  • 系統調用(如果必要)

如果mheap也沒有足夠的內存,那么程序將通過系統調用向操作系統請求更多的虛擬內存空間,然后將這部分空間添加到mheap中,再進行內存分配。

內存單元mspan

mcentral會以我們為某個實例對象所需要分配的內存的大小來建立不同的等級,那么這個大小等級是怎么劃分的?

Golang中有兩個概念,最小的存儲單元-8KB:Page最小的管理單元:mspan

最小的存儲單元也稱為頁,page,大小為8KB

mspan里邊的obj大小,從8B32KB32,768 字節 B)被劃分67種不同的規格【2025年3月10日,go源碼最新確認是67種,大部分教程可能是兩年前時候的,兩年前是66種,https://github.com/golang/go/blob/master/src/runtime/sizeclasses.go ,Go倉庫鏈接。】,分配對象的內存的時候,會根據大小映射到不同規格的mspan。(所以下面的圖劃錯了,最高應該是32KB)

在這里插入圖片描述

mcentral會根據不同mspan的等級,有不同的central的實例,每個實例會以一個雙向鏈表的形式來管理mspan

所以mspan的特性是如圖下所示的。雙向鏈表、起始page、page的頁數等,用來連續標識。

在這里插入圖片描述

前面我們提到,mspan都是page的整數倍,page是8KB大小的,當mspan的obj等級為8B時,那么mspan里邊就需要劃分很多內存塊objectmspan內部的頁是連續的,至少在虛擬內存的視角中是這樣,因為虛擬內存分配了連續的空間給go。

同等級的mspan會從屬同一個mcentral,一個mcentral會把這些同等級的span構造成鏈表,所以上邊是雙向鏈表,有兩個指針。并且使用一個鎖進行互斥,來管理。

mspan會基于位圖算法bitMap來快速找到對應的空閑塊object,塊大小對應等級的大小。使用的是ctz64算法。

也就是下面這個分配示意圖,為0代表被占用,為1代表free可以分配出去。

在這里插入圖片描述
mspan等級被劃分為1-67,67級,此外還有個0級,用于處理特殊對象。

class1,就是8bytes,即8B,也就是一個8B的對象,mspan為8KB時,就代表這個mspan可以分配1024個對象出去。

當分配的對象為0-8B,都會使用calss1對應的span。而不是只有8B剛剛好時才進行分配。這也會導致有內存浪費。

這也會導致一個tail waste,末尾浪費,當class為3時,obj對象大小為24B,那么8KB=8192B的span會不能完全分配完obj,會造成末尾浪費,也就是8B,341x24+8=8192B。

除此之外還有max waste,代表了這個mspan分配的時候最多可能會造成的空間浪費。這個也很好理解,當所有對象為17B的時候,分配了341個出去,那么一共會造成total = (24-17)x341 + 8 空間的浪費,這個總共的空間除以 total / 8192 = 29%,這就是class為3時的 max waste 為 29%。

每個object還有個一個很重要的屬性是nocan,也就是是否object包含了指針,在垃圾回收gc時是否需要展開對應的標記。

在go中,span classnocan 兩個部分信息會組成一個 uint8,形成完整的spanclass標識,8個bit中,高7位標識了span中一共66個等級,最低位標識nocan就可以了。

線程緩存mcache

在這里插入圖片描述

mcache是每個P獨有的緩存,因此交互無鎖。mcache將每種spanClass等級的mspan都各自緩存了一個,同時分為scannocan兩個系列,也就是是否在gc時需要展開。一共是68*2=136個。

mcache還有一個tiny allocator微對象分配器,用于處理小于16B的對象內存分配。(參考了TCMalloc。)

中心緩存mcentral

每個central會對應一種等級的spanClass,然后把spanclass分為兩類,分別是有空間的mspan鏈表partial還有滿空間mspan鏈表full。

每個central會有一把鎖,這就是細化鎖的粒度。可以把mcentral看成是mheap的一部分,只不過會優先從 MCentral 獲取內存,如果沒有 MCentral 會從 Arenas 中的某個 HeapArena 獲取 Page。

在這里插入圖片描述

全局堆緩存mheap

從go上層應用的角度來看,堆就是操作系統虛擬內存的抽象,可以看作是代言人。

mheap以頁為單位,8KB大小,作為最小內存存儲單元。注意與之前講過的span的內存管理單元區分。

基于bitMap標識每個頁的使用情況,每個bit對應一頁,為0就是代表可以用,為1的話代表已經被mspan給分配走,但是不一定已經被obj對象使用了。

mheap有一個聚合頁heapArena,有記錄頁到其所從屬的mspan的映射信息。這是為了方便在gc時進行操作。

建立空閑頁基數樹索引radix tree index,幫助我們能夠快速找到空閑頁。因為我們剛剛說過,mspan中需要的page是連續的,所以如何通過bitMap來快速找到 連續+空閑 的頁page,是需要考慮的,也就是這個的目的,能夠找到符合我們需求數量的空閑頁。


mheapmcentral持有者,持有所有spanClass下的mcentral,作為自身的緩存。可以把mcentral看成mheap更細化粒度的緩存。那么,我們應該如何理解這句話?

首先,mcentral可以看成是一次性從mheap中分配一系列的空間去給上層使用。

也就是說,mheapmcentral的持有者,這意味著mheap負責管理所有的mcentral實例。每個mcentral實例對應一個特定的spanClass,用于緩存特定大小的內存塊。

mcentral作為mheap的緩存,這意味著mheap通過mcentral來間接管理內存塊。當需要分配內存時,首先會檢查相應的mcentral是否有可用的內存塊。如果有,就直接從mcentral中分配;如果沒有,mheap會負責從操作系統申請新的內存,然后將其添加到相應的mcentral中。

當內存不夠時,mheap會向操作系統申請,申請單位為heapArena64M

heapArena

通過下面這個圖來快速知道heapArena的概念。(下面圖中的單詞拼多了一個a,應該為heapArena)
在這里插入圖片描述
我們說過,mheap上游是mcentral,mcentral中的mspan如果不夠了會向mheap申請,mheap下游就是直接跟操作系統虛擬內存對接,mheap如果還不夠,就直接向虛擬內存申請了,一次性申請的大小是heapArena,也就是64MB,訪問mheap的時候需要加鎖,因為是全局唯一的。

mheap是對內存塊的管理對象,通過page為最小內存存儲單元進行管理。一系列的page組合成一個heapAreana

所以,每個 heapArena 包含 8192 個頁,大小為 8192 * 8KB = 64 MB。

heapArena 記錄了頁到 mspan 的映射. 因為 GC 時,通過地址偏移找到頁很方便,但找到其所屬的 mspan 不容易,所以我們需要通過這個映射信息進行輔助。

每個heapArena包含一個bitmap,標記當前這個heapArena的使用情況。主要是為了GC垃圾回收,bitmap有兩種標記,一種是標記對應地址中是否存在對象,另一種標記這個對象是否被GC模塊標記過,所以當前heapArena中所有的Page都會被bitmap標記。

空閑頁索引pageAlloc

到這里已經有些比較繞了,再回顧一下這個圖。注意這個只是邏輯圖!

在這里插入圖片描述

首先,pageAlloc是一種基于基數樹(Radix Tree)索引結構,它用于快速查找和分配空閑頁。pageAlloc通過組織和管理空閑頁的索引信息,優化了內存分配過程中的查找效率,從而提高了內存分配的性能。基數樹是一種高效的數據結構,它能夠快速地定位和檢索數據,這使得pageAlloc能夠迅速找到滿足分配要求的連續頁空間。在內存分配過程中,pageAlloc會根據需要分配的頁數量,在基數樹中查找合適的空閑頁范圍,如果找到合適的空閑頁,就進行分配;如果沒有找到,則可能需要觸發垃圾回收或者向操作系統請求更多的內存資源。

3. Go對象分配

Go中分配對象的方式有幾種常見的,比如new(T)&T{}make(T)等。這幾種方法都會最終通過mallocgc方法進行分配。

Go會根據obj的大小,將對象分為3類,分別是tiny微對象(0,16B)small小對象【16B,32KB】large大對象(32KB以上)

不同類型的對象,會有不同的分配策略,這些分配策略可以在mallocgc 方法中查看。

微對象的分配流程如下。

  1. 從P專屬的mcache的tiny分配器中取對應內存,這個過程是無鎖的。
  2. 根據對應的spanClass,從p專屬mcache緩存mspan中取內存,無鎖。
  3. 根據對應的spanClass從對應的mcentral中取msapn填充到mcache,然后從mspan中取內存,spanClass粒度的鎖。
  4. 根據對應的spanClass,從mheap的頁分配器pageAlloc中取得足夠數量空閑頁組裝成mspan填充到mcentral中,然后再填充到mcache中,然后從mspan中取內存,涉及到了mheap,所以是全局鎖。
  5. mheap向操作系統申請內存,更新頁分配器的索引信息,然后重復步驟4.

小對象的分配流程就是跳過上述的步驟1,直接執行2-5即可。

對于大的對象,跳過步驟1-3,直接執行步驟4和5,因為大對象是0號等級,所以在mcentral里面找不到對應的spanClass等級,只能去從步驟4開始直接與堆進行交互操作。

mallocgc函數

進行對象實例的時候,都會進行mallocgc這個方法。

malloc是內存分配的意思,gc是垃圾回收的意思,這個函數不僅是進行內存分配,還是gc垃圾回收的入口,所以叫做mallocgc

malloc.go代碼如下。

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {// ...    // 獲取 mmp := acquirem()// 獲取當前 p 對應的 mcachec := getMCache(mp)var span *mspanvar 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 + sizec.tinyAllocs++mp.mallocing = 0releasem(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] = 0size = maxTinySize} else {// 根據對象大小,映射到其所屬的 span 的等級(0~66)var sizeclass uint8if 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 中的 spanspan = 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 號 spanspan = c.allocLarge(size, noscan)span.freeindex = 1span.allocCount = 1size = span.elemsize         // 分配空間   x = unsafe.Pointer(span.base())}  // ...return x
}

tiny對象分配內存

P獨有的mcache會有一個微對象分配器,基于offset偏移線性移動的方式對微對象進行分配,每16B是一個塊,對象依據其大小,向上取整為2的整數次冪(2、4、8、16)進行空間補齊,然后進行分配。

在這里插入圖片描述
如果tiny對象分配器沒有分配成功,那么就會到mcache分配。

首先根據對象的大小,映射給其所屬的mspan的等級。對應span等級下,分配給每個對象的空間大小,嘗試獲取mcache中的span,如果分配失敗,就通過mcentral、mheap繼續。

           // 根據對象大小,映射到其所屬的 span 的等級var sizeclass uint8// get size class ....     // 對應 span 等級下,分配給每個對象的空間大小(0~32KB)// 包含了noscan,組裝在一起得到spanClassspc := makeSpanClass(sizeclass, noscan) // 獲取 mcache 中的 spanspan = c.alloc[spc]  // 從 mcache 的 span 中嘗試獲取空間        // 通過ctz64算法,在bit map上找到首個obj空位// 也就是在mspan中,用ctz64算法,根據mspan.allocCache的bitmap信息快速找到空閑的object塊并且返回。v := nextFreeFast(span)if v == 0 {// mcache 分配空間失敗,則通過 mcentral、mheap 繼續           v, span, shouldhelpgc = c.nextFree(spc)}     // 分配空間  x = unsafe.Pointer(v)

mspan也沒有可以分配的obj內存塊的時候,會進入到mcache.nextFree方法進行繼續獲取空間的操作。

也就是上面代碼中的。

          if v == 0 {// mcache 分配空間失敗,則通過 mcentral、mheap 繼續           v, span, shouldhelpgc = c.nextFree(spc)}  

mcentral或者mheap中獲取到了新的span之后,填充到mcachealloc中的span集合當中去,然后再把對應的方法返回。

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
}    

4.結合GMP模型來看內存模型

已經完整的對整個內存模型有了解了,接下來可以結合下GMP來看看內存模型,幫助我們更好的梳理。

再來回顧一下關鍵的一些概念,Page是Go中內存管理與虛擬內存交互內存的最小單元,8KB大小。mspan就是一組連續的Pagemspan的大小是page的整數倍。

mcache是與GMP模型中的P所綁定,而不是線程綁定,真正可運行的線程M的數量與P的數量一致,也就是GOMAXPROCS個。mcache與P綁定可以更節省內存空間的使用,保證每個G使用mcache的時候不需要加鎖就可以獲得內存。

在這里插入圖片描述
實際上我們上層應用向go內存模型取內存,就是從span中分配一個obj出去。在上邊我們已經提到過一次了。

在這里插入圖片描述

span size class 是一塊內存的所屬規模大小,是針對obj size來計劃分的,比如obj1-8B之間的都屬于 size class 1級別,obj大小在8B-16B之間的都數據size Class 2級別。

span size class是 針對span進行劃分的,是span大小的級別,一個span size class 會對應兩個span ,其中一個span存放需要GC掃描的對象,也就是包含了指針的對象,另一個span包含不需要GC的對象。

我們提到過mcache會冗余136個spanClass,也就是68x2,分別對應scan和noscan。

所以mcache的展開內部結構就是這樣對應的關系。協程從mcache上獲取內存不需要加鎖,因為一個P只有一個M(線程)在上面運行,不可能出現競爭,所以沒有鎖的限制,加速了內存的分配。

mcache中每個span class都會對應一個mspan,不同的span classmspan的總大小不一樣,所以需要的page也不一樣。如圖所示,比較清晰能夠看出其中關系。

go對內存規格為0的對象(也就是span class 為0 和1)申請做了特殊處理,也就是更大的內存或者真正的0內存對象,直接會返回一個固定地址,也就是直接跟mheap交互獲得地址,而不會走正常的內存管理邏輯。

如果申請struct{}、[0]int,這種,就會直接返回一個固定地址。

這也是為什么通過channel做同步的時候,發送一個struct{}數據,不會申請任何內存,能夠節省內存空間。

在這里插入圖片描述

協程與mcache的內存交換單位是obj,mcachemcentral的內存交換單位是span

mcentral對于每個級別會存兩個span list鏈表,一個是沒有空間的span list,一個是空的span list。

表示還有可用空間的 Span 鏈表。鏈表中的所有 Span 都至少有 1 個空閑的 Object 空間。如果 mcentral 上游 MCache 退還 Span,會將退還的 Span 加入到 NonEmpty Span List 鏈表中。

tiny對象分配

int32、byte、bool這種tiny微對象如過沒有tiny分配的情況下,會經常申請一個8B的空間,這樣類似bool或者1個字節的byte,也都會獨享這個8B的空間,會造成空間浪費。

如果協程申請的空間小于等于8B,那么會匹配的span size class = 1的8B空間。

而Tiny空間是從span size class =2 中獲取一個16B的obj作為tiny的對象的分配空間。

當大量的微小對象都是用8B的時候會造成大量浪費,所以將小于16B的申請統一歸為tiny微對象申請。然后以字節對齊的方式進行內存分配。

需要注意的是,如果申請的對象有指針,會進入小對象的申請流程(因為需要GC掃描流程),而沒有指針,才會進入tiny微對象申請流程,如果tiny空間的16B沒有多余的內存大小了,會從span size class = 2(也就是第一個noscan的mspan中)申請一個16B的object對象放在tiny空間中。

5.總結

設計思想

下次有機會再梳理一篇TCMalloc的文章。

無論是操作系統虛擬內存管理,還是 C++ 的 TCMalloc、Golang 內存模型,均有一個共同特點,就是分層的緩存機制。

針對不同的內存場景采用不同的獨特解決方式,提高局部性邏輯和細微粒度內存的復用率。這也是程序設計的至高理念。

一些問題?

為什么mcache與P綁定?

這里查閱了一些資料包括GPT,按我的理解應該如下:

首先可以, 減少鎖競爭:由于每個 P 都有自己的 MCache,當多個 goroutine 在不同的 P 上執行時,它們各自的 MCache 是隔離的,不需要加鎖就能獲得內存分配。

另外是,避免內存浪費:如果 MCache 直接與 M 綁定,那么每個線程的內存緩存會相對獨立且會有較高的內存占用。并且最關鍵的是,真正可運行的線程M的數量與P的數量一致,如果mcache與線程綁定,那么很多線程是會空閑的,而不是真正可運行的。所以M可運行的數量因為=P的數量,那么與 P 綁定的話就可以通過合理共享內存緩存來節省內存空間。

span的等級到底是66級還是67級或者68級?

截止目前,3月5日,Go官方github中的代碼注釋是1-67種,算上0,一共是68種,可以看到源代碼的相關參考如下。
[https://github.com/golang/go/blob/master/src/runtime/sizeclasses.go]官方地址如上,很多博客或者資料寫的是1-66種,可能是因為兩年前的版本,是1-66種,目前已經是67種了。

//go:generate go run mksizeclasses.gopackage runtime// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8
//     4         32        8192      256           0     21.88%         32
//     5         48        8192      170          32     31.52%         16
//     6         64        8192      128           0     23.44%         64
//     7         80        8192      102          32     19.07%         16
//     8         96        8192       85          32     15.95%         32
//     9        112        8192       73          16     13.56%         16
//    10        128        8192       64           0     11.72%        128
//    11        144        8192       56         128     11.82%         16
//    12        160        8192       51          32      9.73%         32
//    13        176        8192       46          96      9.59%         16
//    14        192        8192       42         128      9.25%         64
//    15        208        8192       39          80      8.12%         16
//    16        224        8192       36         128      8.15%         32
//    17        240        8192       34          32      6.62%         16
//    18        256        8192       32           0      5.86%        256
//    19        288        8192       28         128     12.16%         32
//    20        320        8192       25         192     11.80%         64
//    21        352        8192       23          96      9.88%         32
//    22        384        8192       21         128      9.51%        128
//    23        416        8192       19         288     10.71%         32
//    24        448        8192       18         128      8.37%         64
//    25        480        8192       17          32      6.82%         32
//    26        512        8192       16           0      6.05%        512
//    27        576        8192       14         128     12.33%         64
//    28        640        8192       12         512     15.48%        128
//    29        704        8192       11         448     13.93%         64
//    30        768        8192       10         512     13.94%        256
//    31        896        8192        9         128     15.52%        128
//    32       1024        8192        8           0     12.40%       1024
//    33       1152        8192        7         128     12.41%        128
//    34       1280        8192        6         512     15.55%        256
//    35       1408       16384       11         896     14.00%        128
//    36       1536        8192        5         512     14.00%        512
//    37       1792       16384        9         256     15.57%        256
//    38       2048        8192        4           0     12.45%       2048
//    39       2304       16384        7         256     12.46%        256
//    40       2688        8192        3         128     15.59%        128
//    41       3072       24576        8           0     12.47%       1024
//    42       3200       16384        5         384      6.22%        128
//    43       3456       24576        7         384      8.83%        128
//    44       4096        8192        2           0     15.60%       4096
//    45       4864       24576        5         256     16.65%        256
//    46       5376       16384        3         256     10.92%        256
//    47       6144       24576        4           0     12.48%       2048
//    48       6528       32768        5         128      6.23%        128
//    49       6784       40960        6         256      4.36%        128
//    50       6912       49152        7         768      3.37%        256
//    51       8192        8192        1           0     15.61%       8192
//    52       9472       57344        6         512     14.28%        256
//    53       9728       49152        5         512      3.64%        512
//    54      10240       40960        4           0      4.99%       2048
//    55      10880       32768        3         128      6.24%        128
//    56      12288       24576        2           0     11.45%       4096
//    57      13568       40960        3         256      9.99%        256
//    58      14336       57344        4           0      5.35%       2048
//    59      16384       16384        1           0     12.49%       8192
//    60      18432       73728        4           0     11.11%       2048
//    61      19072       57344        3         128      3.57%        128
//    62      20480       40960        2           0      6.87%       4096
//    63      21760       65536        3         256      6.25%        256
//    64      24576       24576        1           0     11.45%       8192
//    65      27264       81920        3         128     10.00%        128
//    66      28672       57344        2           0      4.91%       4096
//    67      32768       32768        1           0     12.50%       8192

0級到底是什么?是更大對象嗎?

小徐先生1212教程中寫道0級是為了更大對象的申請,可是更大對象的申請應該是直接跟mheap進行申請,并不是所謂的0級,在劉丹冰老師的博客中,驗證了申請0級span class對象的時候,返回的地址都是一樣的。所以我覺得0級應該是特殊對象,比如struct{}這種,用來做channel通道通信。

我們來看看malloc.go這部分的源碼。可以很清楚的看到,當大于32KB的時候,直接從heap中申請。

而當size==0的時候,直接返回一個zerobase的地址,那么這個zerobase是什么呢?


// 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)
}//……(省略部分代碼)
}

運行下面的測試代碼,看看輸出結果。

//第一篇/chapter3/MyGolang/zeroBase.go
package mainimport (
"fmt"
)func main() {
var (
//0內存對象
a struct{}
b [0]int//100個0內存struct{}
c [100]struct{}//100個0內存struct{},make申請形式
d = make([]struct{}, 100)
)fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c[50])    //取任意元素
fmt.Printf("%p\n", &(d[50]))  //取任意元素
}

運行結果如下,可以看到全部的 0 內存對象分配,返回的都是一個固定的地址。

go run zeroBase.go 
0x11aac78
0x11aac78
0x11aac78
0x11aac78

6. 參考文章

本文撰寫過程中主要有參考以下兩位老師的文章教程,感謝:

劉丹冰老師的Go三關:https://learnku.com/articles/68142
小徐先生1212的教程:https://www.bilibili.com/video/BV1bv411c7bp

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/72010.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/72010.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/72010.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

33.HarmonyOS NEXT NumberBox 步進器高級技巧與性能優化

HarmonyOS NEXT NumberBox 步進器高級技巧與性能優化 一、高級交互設計 1. 組件聯動控制 // 與Slider雙向綁定 State value: number 50Slider({value: this.value,onChange: (v) > this.value v })NumberBox({value: this.value,onChange: (v) > this.value v })2. …

關于ModbusTCP/RTU協議轉Ethernet/IP(CIP)協議的方案

IGT-DSER智能網關模塊支持西門子、倍福(BECKHOFF)、羅克韋爾AB&#xff0c;以及三菱、歐姆龍等各種品牌的PLC之間通訊&#xff0c;支持Ethernet/IP(CIP)、Profinet(S7)&#xff0c;以及FINS、MC等工業自動化常用協議&#xff0c;同時也支持PLC與Modbus協議的工業機器人、智能儀…

通義萬相2.1 × 藍耘智算:AIGC 界的「黃金搭檔」如何重塑創作未來?

在人工智能生成內容&#xff08;AIGC&#xff09;領域&#xff0c;通義萬相2.1與藍耘智算的結合&#xff0c;正以技術協同效應重新定義創作的可能性。這一組合不僅突破了傳統創作工具的效率瓶頸&#xff0c;更通過算法與算力的深度融合&#xff0c;為影視、廣告、游戲、教育等領…

【FreeRTOS】FreeRTOS操作系統在嵌入式單片機上裸機移植

目錄 一 RTOS概述 二 FreeRTOS移植 三 FreeRTOS使用 四 附錄 一 RTOS概述 先了解一些基礎概念&#xff0c;以下內容摘自FreeRTOS官網&#xff08;FreeRTOS? - FreeRTOS?&#xff09;&#xff1a; 【1】RTOS基礎知識 實時操作系統 (RTOS) 是一種體積小巧、確定性強的計算機…

文件包含漏洞第一關

一、什么是文件包含漏洞 1.文件包含漏洞概述 和SQL注入等攻擊方式一樣&#xff0c;文件包含漏洞也是一種注入型漏洞&#xff0c;其本質就是輸入一段用戶能夠控制的腳本或者代碼&#xff0c;并讓服務端執行。 什么叫包含呢&#xff1f;以PHP為例&#xff0c;我們常常把可重復使…

瑞芯微RK3576(1)-硬件設計

過年期間&#xff0c;趁著放假時間做了一款3576的核心板 方案是2G DDR432G emmc 引出所有IO口 關于接口方面&#xff0c;考慮了一段時間&#xff0c;最終決定使用BTB的模式&#xff0c;主要是能夠出更多的IO&#xff0c;方便拆卸&#xff0c;最讓我擔心的是BTB的位置問題 為了…

Java 大視界 -- Java 大數據在智能醫療藥品研發數據分析與決策支持中的應用(126)

&#x1f496;親愛的朋友們&#xff0c;熱烈歡迎來到 青云交的博客&#xff01;能與諸位在此相逢&#xff0c;我倍感榮幸。在這飛速更迭的時代&#xff0c;我們都渴望一方心靈凈土&#xff0c;而 我的博客 正是這樣溫暖的所在。這里為你呈上趣味與實用兼具的知識&#xff0c;也…

JWT的學習

1、HTTP無狀態及解決方案 HTTP一種是無狀態的協議&#xff0c;每次請求都是一次獨立的請求&#xff0c;一次交互之后就是陌生人。 以CSDN為例&#xff0c;先登錄一次&#xff0c;然后瀏覽器退出&#xff0c;這個時候在進入CSDN&#xff0c;按理說服務器是不知道你已經登陸了&…

時序和延時

1、延遲模型的類型 verilog有三種類型的延遲模型&#xff1a;分布延遲 、 集總延遲 、 路徑延遲&#xff08;pin to pin&#xff09; 1.1、 分布延遲 分布延遲是在每個獨立元件的基礎上進行定義的。 module M(output wire out ,input wire a …

SpringBoot基礎Kafka示例

這里將生產者和消費者放在一個應用中 使用的Boot3.4.3 引入Kafka依賴 <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId> </dependency>yml配置 spring:application:name: kafka-1#kafka…

API調試工具的無解困境:白名單、動態IP與平臺設計問題

引言 你是否曾經在開發中遇到過這樣的尷尬情形&#xff1a;你打開了平臺的API調試工具&#xff0c;準備一番操作&#xff0c;結果卻發現根本無法連接到平臺&#xff1f;別急&#xff0c;問題出在調試工具本身。今天我們要吐槽的就是那些神奇的開放平臺API調試工具&#xff0c;…

多方安全計算(MPC)電子拍賣系統

目錄 一、前言二、多方安全計算(MPC)與電子拍賣系統概述2.1 多方安全計算(MPC)的基本概念2.2 電子拍賣系統背景與需求三、MPC電子拍賣系統設計原理3.1 系統總體架構3.2 電子拍賣中的安全協議3.3 數學與算法證明四、數據加解密模塊設計五、GPU加速與系統性能優化六、GUI設計與系…

【Linux篇】初識Linux指令(上篇)

Linux命令世界&#xff1a;從新手到高手的必備指南 一 Linux發展與歷史1.1 Linux起源與發展1.2 Linux與Windows操作系統對比 二 Linux常用操作指令2.1 ls命令 - “List”&#xff08;列出文件)2.2 pwd指令- "打印當前工作目錄"2.3 cd指令 - “Change Directory”&…

編程視界:C++命名空間

目錄 命名空間 為什么要使用命名空間 什么是命名空間 命名空間的使用方式 關鍵點總結 命名空間的嵌套使用 匿名命名空間 跨模塊調用問題 命名空間可以多次定義 總結 首先從C的hello,world程序入手&#xff0c;來認識一下C語言 #include <iostream> using name…

Redux 和 MobX 高頻面試題

Redux 和 MobX 是 React 生態中的兩大狀態管理方案&#xff0c;在面試中常涉及 原理、使用方式、對比、最佳實踐 等方面。以下是 高頻面試題 詳細答案&#xff0c;助你輕松應對面試&#xff01;&#x1f680; &#x1f525; Redux 部分 1. Redux 是什么&#xff1f;為什么需要…

Excel 保護工作簿:它能解決哪些問題?如何正確使用?

在日常辦公中&#xff0c;Excel 表格常常涉及多人協作、重要數據保護&#xff0c;甚至是避免誤操作的情況。這時候&#xff0c;“保護工作簿”功能就能派上用場。它能有效防止他人修改表結構、刪除工作表&#xff0c;甚至可以設置密碼&#xff0c;確保數據的完整性和安全性。今…

Android Retrofit 框架注解定義與解析模塊深度剖析(一)

一、引言 在現代 Android 和 Java 開發中&#xff0c;網絡請求是不可或缺的一部分。Retrofit 作為 Square 公司開源的一款強大的類型安全的 HTTP 客戶端&#xff0c;憑借其簡潔易用的 API 和高效的性能&#xff0c;在開發者社區中廣受歡迎。Retrofit 的核心特性之一便是通過注…

C# Enumerable類 之 數據分組

總目錄 前言 在 C# 中&#xff0c;System.Linq.Enumerable 類是 LINQ&#xff08;Language Integrated Query&#xff09;的核心組成部分&#xff0c;它提供了一系列靜態方法&#xff0c;用于操作實現了 IEnumerable 接口的集合。通過這些方法&#xff0c;我們可以輕松地對集合…

推理模型對SQL理解能力的評測:DeepSeek r1、GPT-4o、Kimi k1.5和Claude 3.7 Sonnet

引言 隨著大型語言模型&#xff08;LLMs&#xff09;在技術領域的應用日益廣泛&#xff0c;評估這些模型在特定技術任務上的能力變得越來越重要。本研究聚焦于四款領先的推理模型——DeepSeek r1、GPT-4o、Kimi k1.5和Claude 3.7 Sonnet在SQL理解與分析方面的能力&#xff0c;…

IDEA接入阿里云百煉中免費的通義千問[2025版]

安裝deepseek 上一篇文章IDEA安裝deepseek最新教程2025中說明了怎么用idea安裝codeGPT插件&#xff0c;并接入DeepSeek&#xff0c;無奈接入的官方api已經不能使用了&#xff0c;所以我們嘗試從其他地方接入 阿里云百煉https://bailian.console.aliyun.com/ 阿里云百煉?是阿…