天天看點

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

Go語言内置運作時(就是runtime),抛棄了傳統的記憶體配置設定方式,改為自主管理。這樣可以自主地實作更好的記憶體使用模式,比如記憶體池、預配置設定等等。這樣,不會每次記憶體配置設定都需要進行系統調用。

Golang運作時的記憶體配置設定算法主要源自 Google 為 C 語言開發的

TCMalloc算法

,全稱

Thread-CachingMalloc

。核心思想就是把記憶體分為多級管理,進而降低鎖的粒度。它将可用的堆記憶體采用二級配置設定的方式進行管理:每個線程都會自行維護一個獨立的記憶體池,進行記憶體配置設定時優先從該記憶體池中配置設定,當記憶體池不足時才會向全局記憶體池申請,以避免不同線程對全局記憶體池的頻繁競争。

為了更好的閱讀體驗,手動貼上文章目錄:

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

基礎概念

Go在程式啟動的時候,會先向作業系統申請一塊記憶體(注意這時還隻是一段虛拟的位址空間,并不會真正地配置設定記憶體),切成小塊後自己進行管理。

申請到的記憶體塊被配置設定了三個區域,在X64上分别是512MB,16GB,512GB大小。

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

arena區域

就是我們所謂的堆區,Go動态配置設定的記憶體都是在這個區域,它把記憶體分割成

8KB

大小的頁,一些頁組合起來稱為

mspan

bitmap區域

辨別

arena

區域哪些位址儲存了對象,并用

4bit

标志位表示對象是否包含指針、

GC

标記資訊。

bitmap

中一個

byte

大小的記憶體對應

arena

區域中4個指針大小(指針大小為 8B )的記憶體,是以

bitmap

區域的大小是

512GB/(4*8B)=16GB

。如下圖:

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

從上圖其實還可以看到bitmap的高位址部分指向arena區域的低位址部分,也就是說bitmap的位址是由高位址向低位址增長的。

spans區域

存放

mspan

(也就是一些

arena

分割的頁組合起來的記憶體管理基本單元,後文會再講)的指針,每個指針對應一頁,是以

spans

區域的大小就是

512GB/8KB*8B=512MB

。除以8KB是計算

arena

區域的頁數,而最後乘以8是計算

spans

區域所有指針的大小。建立

mspan

的時候,按頁填充對應的

spans

區域,在回收

object

時,根據位址很容易就能找到它所屬的

mspan

記憶體管理單元

mspan

:Go中記憶體管理的基本單元,是由一片連續的

8KB

的頁組成的大塊記憶體。注意,這裡的頁和作業系統本身的頁并不是一回事,它一般是作業系統頁大小的幾倍。一句話概括:

mspan

是一個包含起始位址、

mspan

規格、頁的數量等内容的雙端連結清單。

每個

mspan

按照它自身的屬性

SizeClass

的大小分割成若幹個

object

,每個

object

可存儲一個對象。并且會使用一個位圖來标記其尚未使用的

object

。屬性

SizeClass

決定

object

大小,而

mspan

隻會配置設定給和

object

尺寸大小接近的對象,當然,對象的大小要小于

object

大小。還有一個概念:

SpanClass

,它和

SizeClass

的含義差不多,

Size_Class = Span_Class / 2

這是因為其實每個

SizeClass

有兩個

mspan

,也就是有兩個

SpanClass

。其中一個配置設定給含有指針的對象,另一個配置設定給不含有指針的對象。這會給垃圾回收機制帶來利好,之後的文章再談。

如下圖,

mspan

由一組連續的頁組成,按照一定大小劃分成

object

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

Go1.9.2裡

mspan

SizeClass

共有67種,每種

mspan

分割的object大小是8*2n的倍數,這個是寫死在代碼裡的:

// path: /usr/local/go/src/runtime/sizeclasses.go

const _NumSizeClasses = 67

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

根據

mspan

SizeClass

可以得到它劃分的

object

大小。 比如

SizeClass

等于3,

object

大小就是32B。 32B大小的object可以存儲對象大小範圍在17B~32B的對象。而對于微小對象(小于16B),配置設定器會将其進行合并,将幾個對象配置設定到同一個

object

中。

數組裡最大的數是32768,也就是32KB,超過此大小就是大對象了,它會被特别對待,這個稍後會再介紹。順便提一句,類型

SizeClass

為0表示大對象,它實際上直接由堆記憶體配置設定,而小對象都要通過

mspan

來配置設定。

對于mspan來說,它的

SizeClass

會決定它所能分到的頁數,這也是寫死在代碼裡的:

// path: /usr/local/go/src/runtime/sizeclasses.go

const _NumSizeClasses = 67

var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}

比如當我們要申請一個

object

大小為

32B

mspan

的時候,在classtosize裡對應的索引是3,而索引3在

class_to_allocnpages

數組裡對應的頁數就是1。

mspan

結構體定義:

// path: /usr/local/go/src/runtime/mheap.go

type mspan struct {

   //連結清單後向指針,用于将span連結起來

   next *mspan

   //連結清單前向指針,用于将span連結起來

   prev *mspan

   // 起始位址,也即所管理頁的位址

   startAddr uintptr

   // 管理的頁數

   npages uintptr

   // 塊個數,表示有多少個塊可供配置設定

   nelems uintptr

   //配置設定位圖,每一位代表一個塊是否已配置設定

   allocBits *gcBits

   // 已配置設定塊的個數

   allocCount uint16

   // class表中的class ID,和Size Classs相關

   spanclass spanClass  

   // class表中的對象大小,也即塊大小

   elemsize uintptr

}

我們将

mspan

放到更大的視角來看:

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

上圖可以看到有兩個

S

指向了同一個

mspan

,因為這兩個

S

指向的

P

是同屬一個

mspan

的。是以,通過

arena

上的位址可以快速找到指向它的

S

,通過

S

就能找到

mspan

,回憶一下前面我們說的

mspan

區域的每個指針對應一頁。

假設最左邊第一個

mspan

SizeClass

等于10,根據前面的

class_to_size

數組,得出這個

msapn

分割的

object

大小是144B,算出可配置設定的對象個數是

8KB/144B=56.89

個,取整56個,是以會有一些記憶體浪費掉了,Go的源碼裡有所有

SizeClass

mspan

浪費的記憶體的大小;再根據

class_to_allocnpages

數組,得到這個

mspan

隻由1個

page

組成;假設這個

mspan

是配置設定給無指針對象的,那麼

spanClass

等于20。

startAddr

直接指向

arena

區域的某個位置,表示這個

mspan

的起始位址,

allocBits

指向一個位圖,每位代表一個塊是否被配置設定了對象;

allocCount

則表示總共已配置設定的對象個數。

這樣,左起第一個

mspan

的各個字段參數就如下圖所示:

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

記憶體管理元件

記憶體配置設定由記憶體配置設定器完成。配置設定器由3種元件構成:

mcache

,

mcentral

,

mheap

mcache

mcache

:每個工作線程都會綁定一個mcache,本地緩存可用的

mspan

資源,這樣就可以直接給Goroutine配置設定,因為不存在多個Goroutine競争的情況,是以不會消耗鎖資源。

mcache

的結構體定義:

//path: /usr/local/go/src/runtime/mcache.go

type mcache struct {

   alloc [numSpanClasses]*mspan

}

numSpanClasses = _NumSizeClasses << 1

mcache

SpanClasses

作為索引管理多個用于配置設定的

mspan

,它包含所有規格的

mspan

。它是

_NumSizeClasses

的2倍,也就是

67*2=134

,為什麼有一個兩倍的關系,前面我們提到過:為了加速之後記憶體回收的速度,數組裡一半的

mspan

中配置設定的對象不包含指針,另一半則包含指針。

對于無指針對象的

mspan

在進行垃圾回收的時候無需進一步掃描它是否引用了其他活躍的對象。 後面的垃圾回收文章會再講到,這次先到這裡。

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

mcache

在初始化的時候是沒有任何

mspan

資源的,在使用過程中會動态地從

mcentral

申請,之後會緩存下來。當對象小于等于32KB大小時,使用

mcache

的相應規格的

mspan

進行配置設定。

mcentral

mcentral

:為所有

mcache

提供切分好的

mspan

資源。每個

central

儲存一種特定大小的全局

mspan

清單,包括已配置設定出去的和未配置設定出去的。 每個

mcentral

對應一種

mspan

,而

mspan

的種類導緻它分割的

object

大小不同。當工作線程的

mcache

中沒有合适(也就是特定大小的)的

mspan

時就會從

mcentral

擷取。

mcentral

被所有的工作線程共同享有,存在多個Goroutine競争的情況,是以會消耗鎖資源。結構體定義:

//path: /usr/local/go/src/runtime/mcentral.go

type mcentral struct {

   // 互斥鎖

   lock mutex

   // 規格

   sizeclass int32

   // 尚有空閑object的mspan連結清單

   nonempty mSpanList

   // 沒有空閑object的mspan連結清單,或者是已被mcache取走的msapn連結清單

   empty mSpanList

   // 已累計配置設定的對象個數

   nmalloc uint64

}

用圖來表示:

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

empty

表示這條連結清單裡的

mspan

都被配置設定了

object

,或者是已經被

cache

取走了的

mspan

,這個

mspan

就被那個工作線程獨占了。而

nonempty

則表示有空閑對象的

mspan

清單。每個

central

結構體都在

mheap

中維護。

簡單說下

mcache

mcentral

擷取和歸還

mspan

的流程:

  • 擷取 加鎖;從

    nonempty

    連結清單找到一個可用的

    mspan

    ;并将其從

    nonempty

    連結清單删除;将取出的

    mspan

    加入到

    empty

    連結清單;将

    mspan

    傳回給工作線程;解鎖。
  • 歸還 加鎖;将

    mspan

    empty

    連結清單删除;将

    mspan

    加入到

    nonempty

    連結清單;解鎖。

mheap

mheap

:代表Go程式持有的所有堆空間,Go程式使用一個

mheap

的全局對象

_mheap

來管理堆記憶體。

mcentral

沒有空閑的

mspan

時,會向

mheap

申請。而

mheap

沒有資源時,會向作業系統申請新記憶體。

mheap

主要用于大對象的記憶體配置設定,以及管理未切割的

mspan

,用于給

mcentral

切割成小對象。

同時我們也看到,

mheap

中含有所有規格的

mcentral

,是以,當一個

mcache

mcentral

申請

mspan

時,隻需要在獨立的

mcentral

中使用鎖,并不會影響申請其他規格的

mspan

mheap

結構體定義:

//path: /usr/local/go/src/runtime/mheap.go

type mheap struct {

   lock mutex

   // spans: 指向mspans區域,用于映射mspan和page的關系

   spans []*mspan

   // 指向bitmap首位址,bitmap是從高位址向低位址增長的

   bitmap uintptr

   // 訓示arena區首位址

   arena_start uintptr

   // 訓示arena區已使用位址位置

   arena_used  uintptr

   // 訓示arena區末位址

   arena_end   uintptr

   central [67*2]struct {

       mcentral mcentral

       pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte

   }

}

用圖來表示:

ntfs配置設定單元大小_圖解Go語言記憶體配置設定基礎概念記憶體管理單元記憶體管理元件記憶體配置設定流程總結

上圖我們看到,bitmap和arena_start指向了同一個位址,這是因為bitmap的位址是從高到低增長的,是以他們指向的記憶體位置相同。

記憶體配置設定流程

上一篇文章《Golang之變量去哪兒》中我們提到了,變量是在棧上配置設定還是在堆上配置設定,是由逃逸分析的結果決定的。通常情況下,編譯器是傾向于将變量配置設定到棧上的,因為它的開銷小,最極端的就是"zero garbage",所有的變量都會在棧上配置設定,這樣就不會存在記憶體碎片,垃圾回收之類的東西。

Go的記憶體配置設定器在配置設定對象時,根據對象的大小,分成三類:小對象(小于等于16B)、一般對象(大于16B,小于等于32KB)、大對象(大于32KB)。

大體上的配置設定流程:

  • >32KB 的對象,直接從mheap上配置設定;
  • <=16B 的對象使用mcache的tiny配置設定器配置設定;
  • (16B,32KB] 的對象,首先計算對象的規格大小,然後使用mcache中相應規格大小的mspan配置設定;
    • 如果mcache沒有相應規格大小的mspan,則向mcentral申請
    • 如果mcentral沒有相應規格大小的mspan,則向mheap申請
    • 如果mheap中也沒有合适大小的mspan,則向作業系統申請

總結

Go語言的記憶體配置設定非常複雜,它的一個原則就是能複用的一定要複用。源碼很難追,後面可能會再來一篇關于記憶體配置設定的源碼閱讀相關的文章。簡單總結一下本文吧。

文章從一個比較粗的角度來看Go的記憶體配置設定,并沒有深入細節。一般而言,了解它的原理,到這個程度也可以了。

  • Go在程式啟動時,會向作業系統申請一大塊記憶體,之後自行管理。
  • Go記憶體管理的基本單元是mspan,它由若幹個頁組成,每種mspan可以配置設定特定大小的object。
  • mcache, mcentral, mheap是Go記憶體管理的三大元件,層層遞進。mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供所有線程使用;mheap管理Go的所有動态配置設定記憶體。
  • 極小對象會配置設定在一個object中,以節省資源,使用tiny配置設定器配置設定記憶體;一般小對象通過mspan配置設定記憶體;大對象則直接由mheap配置設定記憶體。