溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何分析Go語言內(nèi)存分配

發(fā)布時(shí)間:2022-01-17 17:57:28 來源:億速云 閱讀:110 作者:kk 欄目:數(shù)據(jù)庫

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)如何分析Go語言內(nèi)存分配,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

Go語言內(nèi)置運(yùn)行時(shí)(就是runtime),拋棄了傳統(tǒng)的內(nèi)存分配方式,改為自主管理。這樣可以自主地實(shí)現(xiàn)更好的內(nèi)存使用模式,比如內(nèi)存池、預(yù)分配等等。這樣,不會(huì)每次內(nèi)存分配都需要進(jìn)行系統(tǒng)調(diào)用。

Golang運(yùn)行時(shí)的內(nèi)存分配算法主要源自 Google 為 C 語言開發(fā)的TCMalloc算法,全稱Thread-Caching Malloc。核心思想就是把內(nèi)存分為多級(jí)管理,從而降低鎖的粒度。它將可用的堆內(nèi)存采用二級(jí)分配的方式進(jìn)行管理:每個(gè)線程都會(huì)自行維護(hù)一個(gè)獨(dú)立的內(nèi)存池,進(jìn)行內(nèi)存分配時(shí)優(yōu)先從該內(nèi)存池中分配,當(dāng)內(nèi)存池不足時(shí)才會(huì)向全局內(nèi)存池申請(qǐng),以避免不同線程對(duì)全局內(nèi)存池的頻繁競(jìng)爭(zhēng)。

基礎(chǔ)概念
Go在程序啟動(dòng)的時(shí)候,會(huì)先向操作系統(tǒng)申請(qǐng)一塊內(nèi)存(注意這時(shí)還只是一段虛擬的地址空間,并不會(huì)真正地分配內(nèi)存),切成小塊后自己進(jìn)行管理。

申請(qǐng)到的內(nèi)存塊被分配了三個(gè)區(qū)域,在X64上分別是512MB,16GB,512GB大小。

堆區(qū)總覽

arena區(qū)域就是我們所謂的堆區(qū),Go動(dòng)態(tài)分配的內(nèi)存都是在這個(gè)區(qū)域,它把內(nèi)存分割成8KB大小的頁,一些頁組合起來稱為mspan。

bitmap區(qū)域標(biāo)識(shí)arena區(qū)域哪些地址保存了對(duì)象,并且用4bit標(biāo)志位表示對(duì)象是否包含指針、GC標(biāo)記信息。bitmap中一個(gè)byte大小的內(nèi)存對(duì)應(yīng)arena區(qū)域中4個(gè)指針大?。ㄖ羔槾笮?8B )的內(nèi)存,所以bitmap區(qū)域的大小是512GB/(4*8B)=16GB。

bitmap arena

bitmap arena

從上圖其實(shí)還可以看到bitmap的高地址部分指向arena區(qū)域的低地址部分,也就是說bitmap的地址是由高地址向低地址增長(zhǎng)的。

spans區(qū)域存放mspan(也就是一些arena分割的頁組合起來的內(nèi)存管理基本單元,后文會(huì)再講)的指針,每個(gè)指針對(duì)應(yīng)一頁,所以spans區(qū)域的大小就是512GB/8KB*8B=512MB。除以8KB是計(jì)算arena區(qū)域的頁數(shù),而最后乘以8是計(jì)算spans區(qū)域所有指針的大小。創(chuàng)建mspan的時(shí)候,按頁填充對(duì)應(yīng)的spans區(qū)域,在回收object時(shí),根據(jù)地址很容易就能找到它所屬的mspan。

內(nèi)存管理單元
mspan:Go中內(nèi)存管理的基本單元,是由一片連續(xù)的8KB的頁組成的大塊內(nèi)存。注意,這里的頁和操作系統(tǒng)本身的頁并不是一回事,它一般是操作系統(tǒng)頁大小的幾倍。一句話概括:mspan是一個(gè)包含起始地址、mspan規(guī)格、頁的數(shù)量等內(nèi)容的雙端鏈表。

每個(gè)mspan按照它自身的屬性Size Class的大小分割成若干個(gè)object,每個(gè)object可存儲(chǔ)一個(gè)對(duì)象。并且會(huì)使用一個(gè)位圖來標(biāo)記其尚未使用的object。屬性Size Class決定object大小,而mspan只會(huì)分配給和object尺寸大小接近的對(duì)象,當(dāng)然,對(duì)象的大小要小于object大小。還有一個(gè)概念:Span Class,它和Size Class的含義差不多,

Size_Class = Span_Class / 2
這是因?yàn)槠鋵?shí)每個(gè) Size Class有兩個(gè)mspan,也就是有兩個(gè)Span Class。其中一個(gè)分配給含有指針的對(duì)象,另一個(gè)分配給不含有指針的對(duì)象。這會(huì)給垃圾回收機(jī)制帶來利好,之后的文章再談。

如下圖,mspan由一組連續(xù)的頁組成,按照一定大小劃分成object。

page mspan

Go1.9.2里mspan的Size Class共有67種,每種mspan分割的object大小是8*2n的倍數(shù),這個(gè)是寫死在代碼里的:

// 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}
根據(jù)mspan的Size Class可以得到它劃分的object大小。 比如Size Class等于3,object大小就是32B。 32B大小的object可以存儲(chǔ)對(duì)象大小范圍在17B~32B的對(duì)象。而對(duì)于微小對(duì)象(小于16B),分配器會(huì)將其進(jìn)行合并,將幾個(gè)對(duì)象分配到同一個(gè)object中。

數(shù)組里最大的數(shù)是32768,也就是32KB,超過此大小就是大對(duì)象了,它會(huì)被特別對(duì)待,這個(gè)稍后會(huì)再介紹。順便提一句,類型Size Class為0表示大對(duì)象,它實(shí)際上直接由堆內(nèi)存分配,而小對(duì)象都要通過mspan來分配。

對(duì)于mspan來說,它的Size Class會(huì)決定它所能分到的頁數(shù),這也是寫死在代碼里的:

// 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}
比如當(dāng)我們要申請(qǐng)一個(gè)object大小為32B的mspan的時(shí)候,在class_to_size里對(duì)應(yīng)的索引是3,而索引3在class_to_allocnpages數(shù)組里對(duì)應(yīng)的頁數(shù)就是1。

mspan結(jié)構(gòu)體定義:

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

type mspan struct {
 //鏈表前向指針,用于將span鏈接起來
 next *mspan

//鏈表前向指針,用于將span鏈接起來
 prev *mspan

// 起始地址,也即所管理頁的地址
 startAddr uintptr

// 管理的頁數(shù)
 npages uintptr

// 塊個(gè)數(shù),表示有多少個(gè)塊可供分配
 nelems uintptr

//分配位圖,每一位代表一個(gè)塊是否已分配
 allocBits *gcBits

// 已分配塊的個(gè)數(shù)
 allocCount uint16

// class表中的class ID,和Size Classs相關(guān)
 spanclass spanClass

// class表中的對(duì)象大小,也即塊大小
 elemsize uintptr
}
我們將mspan放到更大的視角來看:

mspan更大視角

上圖可以看到有兩個(gè)S指向了同一個(gè)mspan,因?yàn)檫@兩個(gè)S指向的P是同屬一個(gè)mspan的。所以,通過arena上的地址可以快速找到指向它的S,通過S就能找到mspan,回憶一下前面我們說的mspan區(qū)域的每個(gè)指針對(duì)應(yīng)一頁。

假設(shè)最左邊第一個(gè)mspan的Size Class等于10,根據(jù)前面的class_to_size數(shù)組,得出這個(gè)msapn分割的object大小是144B,算出可分配的對(duì)象個(gè)數(shù)是8KB/144B=56.89個(gè),取整56個(gè),所以會(huì)有一些內(nèi)存浪費(fèi)掉了,Go的源碼里有所有Size Class的mspan浪費(fèi)的內(nèi)存的大??;再根據(jù)class_to_allocnpages數(shù)組,得到這個(gè)mspan只由1個(gè)page組成;假設(shè)這個(gè)mspan是分配給無指針對(duì)象的,那么spanClass等于20。

startAddr直接指向arena區(qū)域的某個(gè)位置,表示這個(gè)mspan的起始地址,allocBits指向一個(gè)位圖,每位代表一個(gè)塊是否被分配了對(duì)象;allocCount則表示總共已分配的對(duì)象個(gè)數(shù)。

這樣,左起第一個(gè)mspan的各個(gè)字段參數(shù)就如下圖所示:

左起第一個(gè)mspan具體值

內(nèi)存管理組件
內(nèi)存分配由內(nèi)存分配器完成。分配器由3種組件構(gòu)成:mcache, mcentral, mheap。

mcache
mcache:每個(gè)工作線程都會(huì)綁定一個(gè)mcache,本地緩存可用的mspan資源,這樣就可以直接給Goroutine分配,因?yàn)椴淮嬖诙鄠€(gè)Goroutine競(jìng)爭(zhēng)的情況,所以不會(huì)消耗鎖資源。

mcache的結(jié)構(gòu)體定義:

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

type mcache struct {
 alloc [numSpanClasses]*mspan
}

numSpanClasses = _NumSizeClasses << 1
mcache用Span Classes作為索引管理多個(gè)用于分配的mspan,它包含所有規(guī)格的mspan。它是_NumSizeClasses的2倍,也就是67*2=134,為什么有一個(gè)兩倍的關(guān)系,前面我們提到過:為了加速之后內(nèi)存回收的速度,數(shù)組里一半的mspan中分配的對(duì)象不包含指針,另一半則包含指針。

對(duì)于無指針對(duì)象的mspan在進(jìn)行垃圾回收的時(shí)候無需進(jìn)一步掃描它是否引用了其他活躍的對(duì)象。 后面的垃圾回收文章會(huì)再講到,這次先到這里。

mcache

mcache在初始化的時(shí)候是沒有任何mspan資源的,在使用過程中會(huì)動(dòng)態(tài)地從mcentral申請(qǐng),之后會(huì)緩存下來。當(dāng)對(duì)象小于等于32KB大小時(shí),使用mcache的相應(yīng)規(guī)格的mspan進(jìn)行分配。

mcentral
mcentral:為所有mcache提供切分好的mspan資源。每個(gè)central保存一種特定大小的全局mspan列表,包括已分配出去的和未分配出去的。 每個(gè)mcentral對(duì)應(yīng)一種mspan,而mspan的種類導(dǎo)致它分割的object大小不同。當(dāng)工作線程的mcache中沒有合適(也就是特定大小的)的mspan時(shí)就會(huì)從mcentral獲取。

mcentral被所有的工作線程共同享有,存在多個(gè)Goroutine競(jìng)爭(zhēng)的情況,因此會(huì)消耗鎖資源。結(jié)構(gòu)體定義:

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

type mcentral struct {
 // 互斥鎖
 lock mutex

// 規(guī)格
 sizeclass int32

// 尚有空閑object的mspan鏈表
 nonempty mSpanList

// 沒有空閑object的mspan鏈表,或者是已被mcache取走的msapn鏈表
 empty mSpanList

// 已累計(jì)分配的對(duì)象個(gè)數(shù)
 nmalloc uint64
}
mcentral

empty表示這條鏈表里的mspan都被分配了object,或者是已經(jīng)被cache取走了的mspan,這個(gè)mspan就被那個(gè)工作線程獨(dú)占了。而nonempty則表示有空閑對(duì)象的mspan列表。每個(gè)central結(jié)構(gòu)體都在mheap中維護(hù)。

簡(jiǎn)單說下mcache從mcentral獲取和歸還mspan的流程:

獲取
加鎖;從nonempty鏈表找到一個(gè)可用的mspan;并將其從nonempty鏈表刪除;將取出的mspan加入到empty鏈表;將mspan返回給工作線程;解鎖。

歸還
加鎖;將mspan從empty鏈表刪除;將mspan加入到nonempty鏈表;解鎖。

mheap
mheap:代表Go程序持有的所有堆空間,Go程序使用一個(gè)mheap的全局對(duì)象_mheap來管理堆內(nèi)存。

當(dāng)mcentral沒有空閑的mspan時(shí),會(huì)向mheap申請(qǐng)。而mheap沒有資源時(shí),會(huì)向操作系統(tǒng)申請(qǐng)新內(nèi)存。mheap主要用于大對(duì)象的內(nèi)存分配,以及管理未切割的mspan,用于給mcentral切割成小對(duì)象。

同時(shí)我們也看到,mheap中含有所有規(guī)格的mcentral,所以,當(dāng)一個(gè)mcache從mcentral申請(qǐng)mspan時(shí),只需要在獨(dú)立的mcentral中使用鎖,并不會(huì)影響申請(qǐng)其他規(guī)格的mspan。

mheap結(jié)構(gòu)體定義:

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

type mheap struct {
 lock mutex

// spans: 指向mspans區(qū)域,用于映射mspan和page的關(guān)系
 spans []*mspan

// 指向bitmap首地址,bitmap是從高地址向低地址增長(zhǎng)的
 bitmap uintptr

// 指示arena區(qū)首地址
 arena_start uintptr

// 指示arena區(qū)已使用地址位置
 arena_used uintptr

// 指示arena區(qū)末地址
 arena_end uintptr

central [67*2]struct {
   mcentral mcentral
   pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
 }
}
mheap

上圖我們看到,bitmap和arena_start指向了同一個(gè)地址,這是因?yàn)閎itmap的地址是從高到低增長(zhǎng)的,所以他們指向的內(nèi)存位置相同。

內(nèi)存分配流程
上一篇文章《Golang之變量去哪兒》中我們提到了,變量是在棧上分配還是在堆上分配,是由逃逸分析的結(jié)果決定的。通常情況下,編譯器是傾向于將變量分配到棧上的,因?yàn)樗拈_銷小,最極端的就是"zero garbage",所有的變量都會(huì)在棧上分配,這樣就不會(huì)存在內(nèi)存碎片,垃圾回收之類的東西。

Go的內(nèi)存分配器在分配對(duì)象時(shí),根據(jù)對(duì)象的大小,分成三類:小對(duì)象(小于等于16B)、一般對(duì)象(大于16B,小于等于32KB)、大對(duì)象(大于32KB)。

大體上的分配流程:

32KB 的對(duì)象,直接從mheap上分配;
<=16B 的對(duì)象使用mcache的tiny分配器分配;
(16B,32KB] 的對(duì)象,首先計(jì)算對(duì)象的規(guī)格大小,然后使用mcache中相應(yīng)規(guī)格大小的mspan分配;

如果mcache沒有相應(yīng)規(guī)格大小的mspan,則向mcentral申請(qǐng)
如果mcentral沒有相應(yīng)規(guī)格大小的mspan,則向mheap申請(qǐng)
如果mheap中也沒有合適大小的mspan,則向操作系統(tǒng)申請(qǐng)
總結(jié)
Go語言的內(nèi)存分配非常復(fù)雜,它的一個(gè)原則就是能復(fù)用的一定要復(fù)用。源碼很難追,后面可能會(huì)再來一篇關(guān)于內(nèi)存分配的源碼閱讀相關(guān)的文章。簡(jiǎn)單總結(jié)一下本文吧。

文章從一個(gè)比較粗的角度來看Go的內(nèi)存分配,并沒有深入細(xì)節(jié)。一般而言,了解它的原理,到這個(gè)程度也可以了。

Go在程序啟動(dòng)時(shí),會(huì)向操作系統(tǒng)申請(qǐng)一大塊內(nèi)存,之后自行管理。
Go內(nèi)存管理的基本單元是mspan,它由若干個(gè)頁組成,每種mspan可以分配特定大小的object。
mcache, mcentral, mheap是Go內(nèi)存管理的三大組件,層層遞進(jìn)。mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供所有線程使用;mheap管理Go的所有動(dòng)態(tài)分配內(nèi)存。
極小對(duì)象會(huì)分配在一個(gè)object中,以節(jié)省資源,使用tiny分配器分配內(nèi)存;一般小對(duì)象通過mspan分配內(nèi)存;大對(duì)象則直接由mheap分配內(nèi)存。

go適合做什么

go是golang的簡(jiǎn)稱,而golang可以做服務(wù)器端開發(fā),且golang很適合做日志處理、數(shù)據(jù)打包、虛擬機(jī)處理、數(shù)據(jù)庫代理等工作。在網(wǎng)絡(luò)編程方面,它還廣泛應(yīng)用于web應(yīng)用、API應(yīng)用等領(lǐng)域。

上述就是小編為大家分享的如何分析Go語言內(nèi)存分配了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI