golang 內存分配深度分析

Base on go 1.13

簡介

golang runtime的另外一大主題就是內存分配器,內存分配策略與協程棧、堆、GC等話題息息相關。

  • 類似於TC malloc的思想;
  • 使用span機制來減少內存碎片,每個span至少爲一個頁(go中的一個page爲8KB),且大小爲頁的整數倍,每一種span用於一個範圍的內存分配需求. 比如16-32byte使用分配32byte的span, 112-128使用分配128byte的span.
  • 一共有67個size範圍, 8byte-32KB;每個size有兩種類型(scan和noscan, 表示分配的對象是否會包含指針,不包含指針的就不用GC scan)
  • 多層次Cache來減少分配的衝突。 per-P無鎖的mcache,全局67*2個對應不同size的span的後備mcentral, 全局1個的mheap.
  • mheap中以 treap 的結構維護空閒連續page. 歸還內存到heap時, 連續地址會進行合併.
  • stack分配也是多層次和多class的.
  • 對象由GC進行回收. sysmon會定時把空餘的內存歸還給操作系統

tcmalloc 的機制可以參考::TCMalloc : Thread-Caching Malloc 論文翻譯

golang的內存分配器雖然思想來源於tcmalloc但是實際上與tcmalloc有很大區別,其中很重要一點是Go 語言被設計爲沒有顯式的內存分配與釋放,完全依靠編譯器與 runtime 的配合來自動處理,因此也就造就爲了內存分配器、垃圾回收器兩大組件。

在計算機領域性能優化基本離不開空間換時間,時間換空間,統一管理內存會提前分配或一次性釋放一大塊內存,進而減少與操作系統溝通造成的開銷,進而提高程序的運行性能。支持內存管理另一個優勢就是能夠更好的支持垃圾回收,這一點我們留到垃圾回收器一節中進行討論。

內存分配器主要結構

核心的結構就是:

  • heapArena: 保留整個虛擬地址空間
  • mspan:是 mheap 上管理的一連串的頁
  • mheap:分配的堆,在頁大小爲 8KB 的粒度上進行管理
  • mcentral:蒐集了給定大小等級的所有 span
  • mcache:爲 per-P 的緩存。

頁是向操作系統申請內存的最小單位,目前設計爲 8kb。

這些結構之間的關係比較複雜,後面我們將一點點梳理他們之間的關係。

在golang裏面內存分爲部分,傳統意義上的棧由 runtime 統一管理,用戶態不感知。而傳統意義上的堆內存,又被 Go 運行時劃分爲了兩個部分,

  • 一個是 Go 運行時自身所需的堆內存,即堆外內存;
  • 另一部分則用於 Go 用戶態代碼所使用的堆內存,也叫做 Go 堆。

Go 堆負責了用戶態對象的存放以及 goroutine 的執行棧。

heapArena

Golang 的堆由很多個 arena 組成,每個 arena 在 64 位機器上是 64MB,且起始地址與 arena 的大小對齊,
所有的 arena 覆蓋了整個 Golang 堆的地址空間。

heapArena 對象存儲了一個 heap arena的元數據,heapArena對象自身存儲在Go heap之外,並且通過mheap_.arenas index 來訪問。heapArena對象直接從操作系統分配的,所以理想情況下應該是系統頁面大小的倍數。

const(
pageSize = 8192//8KB
heapArenaBytes = 67108864 //一個heapArena是64MB
heapArenaBitmapBytes = heapArenaBytes / 32 // 一個heapArena的bitmap佔用2MB
pagesPerArena = heapArenaBytes / pageSize  // 一個heapArena包含8192個頁
)

//go:notinheap
type heapArena struct {
	bitmap [heapArenaBitmapBytes]byte //2,097,152
	spans [pagesPerArena]*mspan //
	pageInUse [pagesPerArena / 8]uint8
	pageMarks [pagesPerArena / 8]uint8
}
  • bitmap:是一個2MB個byte數組來標記這個heap area 64M 內存的使用情況,bitmap位圖主要爲GC標記數組,用2bits標記8(PtrSize) 個byte的使用情況。之所以用2個bits,一是標記對應地址中是否存在對象,另外是標記此對象是否被gc標記過。一個功能一個bit位,所以, heap bitmaps用兩個bit位;
  • spans:是一個8192(pagesPerArena)大小的指針數組,每個mspan是8KB;
  • pageInUse:是一個位圖,使用1024 * 8 bit來標記 8192個頁(8192*8KB = 64MB)中哪些頁正在使用中;
  • pageMarks:標記頁,與GC相關;

簡而言之,heapArena 描述了一個 heap arena 的元信息。

arenaHint

arenaHint結構比較簡單,是 arenaHint 鏈表的節點結構,保存了arena 的起始地址、是否爲最後一個 arena,以及下一個 arenaHint 指針。

//go:notinheap
type arenaHint struct {
	addr uintptr
	down bool
	next *arenaHint
}

mspan

前面說了,heapArena 的內存大小是64M,直接管理這麼粗粒度的內存明顯不符合實踐。golang使用span機制來減少碎片. 每個span至少分配1個page(8KB), 劃分成固定大小的slot, 用於分配一定大小範圍的內存需求,小於 32kb 的小對象則分配在固定大小等級的 span 上,否則直接從 mheap 上進行分配。

mspan 是相同大小等級的 span 的雙向鏈表的一個節點,每個節點還記錄了自己的起始地址、指向的 span 中頁的數量。

//go:notinheap
type mspan struct {
	next *mspan     // next span in list, or nil if none
	prev *mspan     // previous span in list, or nil if none

	startAddr uintptr // address of first byte of span aka s.base()
	npages    uintptr // number of pages in span
	//......
	freeindex uintptr
	//......
	allocCount  uint16        // number of allocated objects
	spanclass   spanClass     // size class and noscan (uint8)
	state       mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
	elemsize    uintptr       // computed from sizeclass or from npages
}
  • npages:表示當前span包含多少個頁,npages是根據spanclass來確定的。前面說過了,一個頁是8k,也就是這個span存儲的是 npages*8k 大小內存。
  • spanclass:spanClass是一個uint8,用於計算當前span分配對象的大小。spanClass 的值爲0-66,每一個值分別對應一個分配對象的大小以及頁數。比如spanclass爲1,則span用於分配8個字節的對象,且當前span佔用一個頁的存儲,也就是span是8kb。
  • elemsize:表示分配對象的size,根據spanclass和npages都能夠算出來。

這裏舉一個例子:32byte的span,span佔用一個頁,所以總共有256個slot:mspan-example

  1. 這裏表示slot大小爲32byte的span, 上一次gc之後, 前8個slot使用如上.
  2. freeindex表示 <該位置的都被分配了, >=該位置的可能被分配, 也可能沒有. 配合allocCache來尋找. 每次分配後, freeindex設置爲分配的slot+1.
  3. allocBits表示上一次GC之後哪一些slot被使用了. 0未使用或釋放, 1已分配.
  4. allocCache表示從freeindex開始的64個slot的分配情況, 1爲未分配, 0爲分配. 使 用ctz(Count Trailing Zeros指令)來找到第一個非0位. 使用完了就從allocBits加載, 取 反.
  5. 每次gc完之後, sweep階段, 將allocBits設置爲gcmarkBits.

前面一直都說,spanclass可以確定當前span的page數以及分配的對象的大小:

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

const (
	_MaxSmallSize   = 32768
	smallSizeDiv    = 8
	smallSizeMax    = 1024
	largeSizeDiv    = 128
	_NumSizeClasses = 67
	_PageShift      = 13
)

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}
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}

主要就是數組class_to_size和數組class_to_allocnpages。 數組的size 都是67,也就是0-66。index對應的value分別就是對象的大小以及span佔用page數目。

class0表示分配一個>32KB對象的span, 有67個 size, 每個size兩種, 用於分配有指針和無指針對象, 所以代碼裏面總共有67*2=134個class。比如 spanClass 是1,那麼對應分配對象就是8bytes,然後一個span佔用 一個頁內存,也就是8Kb。

mcentral

// Central list of free objects of a given size.
//go:notinheap
type mcentral struct {
	lock      mutex
	spanclass spanClass
	nonempty  mSpanList // list of spans with a free object, ie a nonempty free list
	empty     mSpanList // list of spans with no free objects (or cached in an mcache)

	// nmalloc is the cumulative count of objects allocated from
	// this mcentral, assuming all spans in mcaches are
	// fully-allocated. Written atomically, read under STW.
	nmalloc uint64
}

當 mcentral 中 nonempty 列表中也沒有可分配的 span 時,則會向 mheap 提出請求,從而獲得新的 span,並進而交給 mcache。

mcache

mcache是一個 per-P 的緩存,它是一個包含不同大小等級的 span 鏈表的數組,其中 mcache.alloc 的每一個數組元素都是某一個特定大小的 mspan 的鏈表頭指針。

const numSpanClasses = _NumSizeClasses << 1 // means (67<<1)

// Per-thread (in Go, per-P) cache for small objects.
// No locking needed because it is per-thread (per-P).
//
// mcaches are allocated from non-GC'd memory, so any heap pointers
// must be specially handled.
//
//go:notinheap
type mcache struct {
	......
	// Allocator cache for tiny objects w/o pointers.
	// See "Tiny allocator" comment in malloc.go.

	// tiny points to the beginning of the current tiny block, or
	// nil if there is no current tiny block.
	//
	// tiny is a heap pointer. Since mcache is in non-GC'd memory,
	// we handle it by clearing it in releaseAll during mark
	// termination.
	tiny             uintptr
	tinyoffset       uintptr
	local_tinyallocs uintptr // number of tiny allocs not counted in other stats

	// The rest is not accessed on every malloc.
	alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass

	stackcache [_NumStackOrders]stackfreelist

	// Local allocator stats, flushed during GC.
	local_largefree  uintptr                  // bytes freed for large objects (>maxsmallsize)
	local_nlargefree uintptr                  // number of frees for large objects (>maxsmallsize)
	local_nsmallfree [_NumSizeClasses]uintptr // number of frees for small objects (<=maxsmallsize)
	......
}

當 mcache 中 span 的數量不夠使用時,會向 mcentral 的 nonempty 列表中獲得新的 span。

mheap

//go:notinheap
type mheap struct {
	lock      mutex
	free      mTreap // free spans
	......
	allspans []*mspan // all spans out there
	......
	arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
	......
	arenaHints *arenaHint
	arena linearAlloc
	......
	// central free lists for small size classes.
	// the padding makes sure that the mcentrals are
	// spaced CacheLinePadSize bytes apart, so that each mcentral.lock
	// gets its own cache line.
	// central is indexed by spanClass.
	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
	}

	spanalloc             fixalloc // allocator for span*
	cachealloc            fixalloc // allocator for mcache*
	treapalloc            fixalloc // allocator for treapNodes*
	specialfinalizeralloc fixalloc // allocator for specialfinalizer*
	specialprofilealloc   fixalloc // allocator for specialprofile*
	speciallock           mutex    // lock for special record allocators.
	arenaHintAlloc        fixalloc // allocator for arenaHints
	......
}

各種結構之間的關係

在這裏插入圖片描述
heap是中間的一行:

  • 其中最中間的灰色區域 arena 覆蓋了 Go 程序的整個虛擬內存,每個 arena 包括一段 bitmap 和一段指向連續 span 的指針;
  • 每個 span 由一串連續的頁組成;
  • 每個 arena 的起始位置通過 arenaHint 進行記錄。

分配的整體順序是從右向左,代價也越來越大。

  • 小對象和微對象優先從白色區域 per-P 的 mcache 分配 span,這個過程不需要加鎖(白色);
  • 若失敗則會從 mheap 持有的 mcentral 加鎖獲得新的 span,這個過程需要加鎖,但只是局部(灰色);
  • 若仍失敗則會從右側的 free 或 scav 進行分配,這個過程需要對整個 heap 進行加鎖,代價最大(黑色)。

內存分配入口

golang程序的運行是基於 goroutine 的,goroutine 和傳統意義上的程序一樣,也有棧和堆的概念,在
Go runtime 內部分別對應:goroutine 執行棧以及 Go 堆。goroutine 的執行棧和我們傳統意義上的棧一樣,當函數返回時,在棧的對象都會被自動回收,從而無需 GC 的標記;而堆則麻煩一些,Go 支持垃圾回收,只要對象生存在堆上,Go 的runtime GC 會在後臺自動進行標記、整理以及在垃圾回收時候回收內存,GC的存在會導致額外的開銷。

舉個簡單的程序:

func f1() *int {
	y := 2
	return &y
}

func main() {
	y := f1()
	println(y)
}

go build -gcflags '-m -l -N' memory_alloc.go
輸出:

# command-line-arguments
./memory_alloc.go:5:2: moved to heap: y

我們看到變量y分配到了heap上。我們再看看 f1() 彙編代碼:

"".f1 STEXT size=79 args=0x8 locals=0x18
......
	0x0024 00036 (memory_alloc.go:5)	MOVQ	AX, (SP)
	0x0028 00040 (memory_alloc.go:5)	CALL	runtime.newobject(SB)
	0x002d 00045 (memory_alloc.go:5)	PCDATA	$0, $1
......

可以發現,對於產生在 golang 堆上分配對象的情況,均調用了運行時的 runtime.newobject 方法。
當然,關鍵字 new 同樣也會被編譯器翻譯爲此函數。
所以 runtime.newobject 就是內存分配的核心入口了。

下面我們看 runtime.newobject 實現:

func newobject(typ *_type) unsafe.Pointer {
	return mallocgc(typ.size, typ, true)
}

const (
	maxSmallSize = 32768 //32kb
	maxTinySize = 16 //16byte
)

// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// 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)
	}
	......
	// Set mp.mallocing to keep from being preempted by GC.
	mp := acquirem()
	......
	mp.mallocing = 1
	......
	// 獲取當前 g 所在 M 所綁定 P 的 mcache
	c := gomcache()
	var x unsafe.Pointer
	noscan := typ == nil || typ.ptrdata == 0
	if size <= maxSmallSize {
		if noscan && size < maxTinySize {
			// 微對象分配
			......
		} else {
			// 小對象分配
			......
		}
	} else {
		// 大對象分配
		......
	}
	......
	mp.mallocing = 0
	releasem(mp)
	......
	return x
}

其中 _type 爲 Go 類型的實現,通過其 size 屬性能夠獲得該類型所需要的大小。

在分配過程中,我們會發現需要持有 M 纔可進行分配,這是因爲分配不僅可能涉及 mcache,還需要將正在分配的 M 標記爲 mallocing,用於記錄當前 M 的分配狀態。

小對象分配
當對一個小對象(<32kB)分配內存時,會將該對象所需的內存大小調整到某個能夠容納該對象的大小等級(size class),並查看 mcache 中對應等級的 mspan,通過掃描 mspan 的 freeindex 來確定是否能夠進行分配。

當沒有可分配的 mspan 時,會從 mcentral 中獲取一個所需大小空間的新的 mspan,從 mcentral 中分配會對其進行加鎖,但一次性獲取整個 span 的過程均攤了對 mcentral 加鎖的成本。

如果 mcentral 的 mspan 也爲空時,則它也會發生增長,從而從 mheap 中獲取一連串的頁,作爲一個新的 mspan 進行提供。而如果 mheap 仍然爲空,或者沒有足夠大的對象來進行分配時,則會從操作系統中分配一組新的頁(至少 1MB),從而均攤與操作系統溝通的成本。

微對象分配
對於過小的微對象(<16B),它們的分配過程與小對象的分配過程基本類似,但是是直接存儲在 mcache 上,並由其以 16B 的塊大小直接進行管理和釋放

大對象分配
大對象分配非常粗暴,不與 mcache 和 mcentral 溝通,直接繞過並通過 mheap 進行分配。

分配組件

這裏主要是說 fixallocfixalloc 是一個基於自由列表的固定大小的分配器。其核心原理是將若干未分配的內存塊連接起來,將未分配的區域的第一個字爲指向下一個未分配區域的指針使用。

Go 的主分配堆中 malloc(span、cache、treap、finalizer、profile、arena hint 等) 均圍繞它爲實體進行固定分配和回收。

fixalloc 作爲抽象,非常簡潔,只包含三個基本操作:初始化、分配、回收

type fixalloc struct {
	size   uintptr
	first  func(arg, p unsafe.Pointer) // called first time p is returned
	arg    unsafe.Pointer
	list   *mlink
	chunk  uintptr // use uintptr instead of unsafe.Pointer to avoid write barriers
	nchunk uint32
	inuse  uintptr // in-use bytes now
	stat   *uint64
	zero   bool // zero allocations
}

fixalloc 是一個簡單的固定大小對象的自由表內存分配器。
Malloc 使用 fixalloc 來管理其 MCache 和 MSpan 對象。
fixalloc.alloc 返回的內存默認爲零值,但調用者可以通過將 zero 標誌設置爲 false來自行負責將分配歸零。如果這部分內存永遠不包含堆指針,則這樣的操作是安全的。
調用方負責鎖定 fixalloc 調用。調用方可以在對象中保持狀態,但當釋放和重新分配時第一個字會被破壞。

fixalloc初始化

Go 語言對於零值有自己的規定,自然也就體現在內存分配器上。而 fixalloc 作爲內存分配器內部組件的來源於
操作系統的內存,自然需要自行初始化,因此,fixalloc 的初始化也就不可避免的需要將自身的各個字段歸零:

func (f *fixalloc) init(size uintptr, first func(arg, p unsafe.Pointer), arg unsafe.Pointer, stat *uint64) {
	f.size = size
	f.first = first
	f.arg = arg
	f.list = nil
	f.chunk = 0
	f.nchunk = 0
	f.inuse = 0
	f.stat = stat
	f.zero = true
}
  • 初始化 f 來分配給定大小的對象,對象大小是入參 size;
  • 分配器按 chunk 獲取內存大小

分配

fixalloc 基於自由表策略進行實現,分爲兩種情況:

  1. 存在被釋放、可複用的內存
  2. 不存在可複用的內存

對於第一種情況,也就是在運行時內存被釋放,但這部分內存並不會被立即回收給操作系統,
我們直接從自由表中獲得即可,但需要注意按需將這部分內存進行清零操作。

對於第二種情況,我們直接向操作系統申請固定大小的內存,然後扣除分配的大小即可。

const(
	_FixAllocChunk = 16 << 10 // 16kb, Chunk size for FixAlloc
)

func (f *fixalloc) alloc() unsafe.Pointer {
	//使用之前必須先初始化
	if f.size == 0 {
		print("runtime: use of FixAlloc_Alloc before FixAlloc_Init\n")
		throw("runtime: internal error")
	}
	// 如果 f.list 不是 nil, 則說明還存在已經釋放、可複用的內存,直接將其分配
	if f.list != nil {
		v := unsafe.Pointer(f.list)
		f.list = f.list.next
		// 更新已使用的內存
		f.inuse += f.size
		if f.zero {
			//如果需要對內存清零,則對取出的內存執行初始化
			memclrNoHeapPointers(v, f.size)
		}
		return v
	}
	//如果此時 nchunk 不足以分配一個 size
	if uintptr(f.nchunk) < f.size {
		// 則向操作系統申請內存,大小爲 16 << 10 pow(2,14)
		f.chunk = uintptr(persistentalloc(_FixAllocChunk, 0, f.stat))
		f.nchunk = _FixAllocChunk
	}
	// 指向申請好的內存
	v := unsafe.Pointer(f.chunk)
	if f.first != nil {
		f.first(f.arg, v)
	}
	// 扣除並保留 size 大小的空間
	f.chunk = f.chunk + f.size
	f.nchunk -= uint32(f.size)
	f.inuse += f.size // 記錄已經使用的大小
	return v
}

回收

回收就更加簡單了,直接將回收的地址指針放回到自由表中即可:

func (f *fixalloc) free(p unsafe.Pointer) {
	// 減少使用的字節數
	f.inuse -= f.size
	// 將要釋放的內存地址作爲 mlink 指針插入到 f.list 內,完成回收
	v := (*mlink)(p)
	// 頭插入自由列表
	v.next = f.list
	f.list = v
}

系統級內存管理調用

系統級的內存管理調用是平臺相關的,這裏以 Linux 爲例,運行時的 sysAllocsysUnusedsysUsedsysFreesysReservesysMapsysFault 都是系統級的調用。

其中 sysAllocsysReservesysMap 都是向操作系統申請內存的操作,他們均涉及關於內存分配的系統調用就是 mmap,區別在於:

  • sysAlloc 是從操作系統上申請清零後的內存,調用參數是 _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE
  • sysReserve 是從操作系統中保留內存的地址空間,並未直接分配內存,調用參數是 _PROT_NONE, _MAP_ANON|_MAP_PRIVATE,;
  • sysMap 則是用於通知操作系統使用先前已經保留好的空間,參數是 _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE

不過 sysAllocsysReserve 都是操作系統對齊的內存,但堆分配器可能使用更大的對齊方式,因此這部分獲得的內存都需要額外進行一些重排的工作。

初始化

這裏來講一下 heap 的初始化。除去執行棧外,內存分配器是最先完成初始化的,我們先來看這個初始化的過程。

內存分配器的初始化除去一些例行的檢查之外,就是對堆的初始化了:

func mallocinit() {
	// 一些涉及內存分配器的常量的檢查,包括
	// heapArenaBitmapBytes, physPageSize 等等
	......
	// 初始化堆
	mheap_.init()
	_g_ := getg()
	_g_.m.mcache = allocmcache()

	// 創建初始的 arena 增長 hint
	if sys.PtrSize == 8 {
		for i := 0x7f; i >= 0; i-- {
			var p uintptr
			switch {
			case GOARCH == "arm64" && GOOS == "darwin":
				p = uintptr(i)<<40 | uintptrMask&(0x0013<<28)
			case GOARCH == "arm64":
				p = uintptr(i)<<40 | uintptrMask&(0x0040<<32)
			case GOOS == "aix":
				if i == 0 {
					continue
				}
				p = uintptr(i)<<40 | uintptrMask&(0xa0<<52)
			case raceenabled:
				p = uintptr(i)<<32 | uintptrMask&(0x00c0<<32)
				if p >= uintptrMask&0x00e000000000 {
					continue
				}
			default:
				p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)
			}
			hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
			hint.addr = p
			hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
		}
	} else {
		......
		// 32 位機器,不關心
	}
}

//初始化堆:
func (h *mheap) init() {
	//初始化堆中各個組件的分配器
	h.treapalloc.init(unsafe.Sizeof(treapNode{}), nil, nil, &memstats.other_sys)
	h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys)
	h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys)
	h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys)
	h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys)
	h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys)

	// 不對 mspan 的分配清零,後臺掃描可以通過分配它來併發的檢查一個 span
	// 因此 span 的 sweepgen 在釋放和重新分配時候能存活,從而可以防止後臺掃描
	// 不正確的將其從 0 進行 CAS。
	// 因爲 mspan 不包含堆指針,因此它是安全的
	h.spanalloc.zero = false

	// h->mapcache needs no init
	for i := range h.central {
		h.central[i].mcentral.init(spanClass(i))
	}
}
//初始化 mcentral
func (c *mcentral) init(spc spanClass) {
	c.spanclass = spc
	c.nonempty.init()
	c.empty.init()
}

在這個過程中還包含對 mcache 初始化 allocmcache(),這個 mcache 會在 procresize 中將 mcache
轉移到 P 的門下,而並非屬於 M。

mcache 的初始化:

func allocmcache() *mcache {
	var c *mcache
	systemstack(func() {
		lock(&mheap_.lock)
		c = (*mcache)(mheap_.cachealloc.alloc())
		c.flushGen = mheap_.sweepgen
		unlock(&mheap_.lock)
	})
	for i := range c.alloc {
		// 暫時指向虛擬的 mspan 中
		c.alloc[i] = &emptymspan
	}
	c.next_sample = nextSample()
	return c
}

運行時的 runtime.allocmcachemheap 上分配一個 mcache
由於 mheap 是全局的,因此在分配期必須對其進行加鎖,而分配通過 fixAlloc 組件完成:

mcache 的釋放

由於 mcache 從非 GC 內存上進行分配,因此出現的任何堆指針都必須進行特殊處理。
所以在釋放前,需要調用 mcache.releaseAll 將堆指針進行處理:

func freemcache(c *mcache) {
	systemstack(func() {
		c.releaseAll()
		stackcache_clear(c)

		lock(&mheap_.lock)
		purgecachedstats(c)
		mheap_.cachealloc.free(unsafe.Pointer(c))
		unlock(&mheap_.lock)
	})
}

func (c *mcache) releaseAll() {
	for i := range c.alloc {
		s := c.alloc[i]
		if s != &emptymspan {
			// 將 span 歸還
			mheap_.central[i].mcentral.uncacheSpan(s)
			c.alloc[i] = &emptymspan
		}
	}
	// 清空 tinyalloc 池.
	c.tiny = 0
	c.tinyoffset = 0
}

func freemcache(c *mcache) {
	systemstack(func() {
		// 歸還 span
		c.releaseAll()
		// 釋放 stack
		stackcache_clear(c)

		lock(&mheap_.lock)
		// 記錄局部統計
		purgecachedstats(c)
		// 將 mcache 釋放
		mheap_.cachealloc.free(unsafe.Pointer(c))
		unlock(&mheap_.lock)
	})
}

per-P? per-M?

首先,mcache 是一個 per-P 的 mcache,我們很自然的疑問就是,爲什麼這個 mcache 在 p/m 這兩個結構體上都有成員?

那麼 mcache 是跟着誰跑的?結合調度器的知識不難發現,m 在執行時需要持有一個 p 才具備執行能力。
有利的證據是,當調用 runtime.procresize 時,初始化新的 P 時,mcache 是直接分配到 p 的;回收 p 時,mcache 是直接從 p 上獲取:

func procresize(nprocs int32) *p {
	(...)
	// 初始化新的 P
	for i := int32(0); i < nprocs; i++ {
		pp := allp[i]
		(...)
		// 爲 P 分配 cache 對象
		if pp.mcache == nil {
			if old == 0 && i == 0 {
				if getg().m.mcache == nil {
					throw("missing mcache?")
				}
				pp.mcache = getg().m.mcache
			} else {
				// 創建 cache
				pp.mcache = allocmcache()
			}
		}

		(...)
	}

	// 釋放未使用的 P
	for i := nprocs; i < old; i++ {
		p := allp[i]
		(...)
		// 釋放當前 P 綁定的 cache
		freemcache(p.mcache)
		p.mcache = nil
		(...)
	}
	(...)
}

因而我們可以明確:

  • mcache 會被 P 持有,當 M 和 P 綁定時,M 同樣會保留 mcache 的指針
  • mcache 直接向操作系統申請內存,且常駐運行時
  • P 通過 make 命令進行分配,會分配在 Go 堆上

對象分配全景:

多層次Cache來減少分配的衝突, 加快分配. 從無鎖到粒度較低的鎖, 再到全局一個鎖, 或系統調用.
在這裏插入圖片描述
分配策略

  1. new, make最終調用mallocgc
  2. >32KB對象, 直接從mheap中分配, 構成一個span
  3. <16byte且無指針(noscan), 使用tiny分配器, 合併分配.
  4. <16byte有指針或16byte-32KB, 如果mcache中有對應 class的空閒mspan, 則直接從該mspan中分配一個slot.
  5. (mcentral.cacheSpan) mcache沒有對應的空餘span, 則從 對應mcentral中申請一個有空餘slot的span到mcache中. 再進行分配
  6. ( mcentral.grow)對應mcentral沒有空餘span, 則向 mheap( mheap_.alloc)中申請一個span, 能sweep出span則返 回. 否則看mheap的free mTreap能否分配最大於該size的連續 頁, 能則分配, 多的頁放回 .
  7. mheap的free mTreap無可用, 則調用sysAlloc(mmap)向系統申請.
  8. 6, 7步中獲得的內存構建成span, 返回給mcache, 分配對象.

大對象分配

大對象(large object)(>32kb)會直接從 golang 堆上進行分配,不涉及 mcache/mcentral/mheap 之間的三級過程,所以過程相對簡單。

// mallocgc 函數裏面大對象分配
var s *mspan
shouldhelpgc = true
systemstack(func() {
	s = largeAlloc(size, needzero, noscan)
})
s.freeindex = 1
s.allocCount = 1
x = unsafe.Pointer(s.base())
size = s.elemsize

可以看到,大對象分配時候會先切換到系統棧,然後調用 largeAlloc 進行分配 mspan 。

func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {
	// 對象太大,溢出
	if size+_PageSize < size {
		throw("out of memory")
	}
	// 根據分配的大小計算需要分配的頁數
	npages := size >> _PageShift
	if size&_PageMask != 0 {
		npages++
	}
	......
	// 從堆上分配
	s := mheap_.alloc(npages, makeSpanClass(0, noscan), true, needzero)
	if s == nil {
		throw("out of memory")
	}
	s.limit = s.base() + size
	......
	return s
}

func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
	return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}

從堆上分配調用了 alloc 方法,這個方法需要指明要分配的頁數、span 的大小等級、是否爲大對象、是否清零。

// 從GC的堆上申請分配N個頁的一個新的span
func (h *mheap) alloc(npage uintptr, spanclass spanClass, large bool, needzero bool) *mspan {
	var s *mspan
	systemstack(func() {
		s = h.alloc_m(npage, spanclass, large)
	})

	if s != nil {
		// 需要清零時,對分配的 span 進行清零
		if needzero && s.needzero != 0 {
			memclrNoHeapPointers(unsafe.Pointer(s.base()), s.npages<<_PageShift)
		}
		s.needzero = 0
	}
	return s

alloc_m 是實際實現,在系統棧上執行:

// alloc_m is the internal implementation of mheap.alloc.
//
// alloc_m must run on the system stack because it locks the heap, so
// any stack growth during alloc_m would self-deadlock.
//
//go:systemstack
func (h *mheap) alloc_m(npage uintptr, spanclass spanClass, large bool) *mspan {
	_g_ := getg()
	......
	lock(&h.lock)

	s := h.allocSpanLocked(npage, &memstats.heap_inuse)
	if s != nil {
		// Record span info, because gc needs to be
		// able to map interior pointer to containing span.
		......
		// Initialize mark and allocation structures.
		s.freeindex = 0
		s.allocCache = ^uint64(0) // all 1s indicating all free.
		s.nelems = nelems
		s.gcmarkBits = gcmarkBits
		s.allocBits = allocBits

		s.state.set(mSpanInUse)

		// Mark in-use span in arena page bitmap.
		arena, pageIdx, pageMask := pageIndexOf(s.base())
		arena.pageInUse[pageIdx] |= pageMask

		// update stats, sweep lists
		h.pagesInUse += uint64(npage)
		if large {
			memstats.heap_objects++
			mheap_.largealloc += uint64(s.elemsize)
			mheap_.nlargealloc++
			atomic.Xadd64(&memstats.heap_live, int64(npage<<_PageShift))
		}
	}
	......
	unlock(&h.lock)
	return s
}

allocSpanlocked 用來從堆上根據頁數來進行實際的分配工作:

// Allocates a span of the given size.  h must be locked.
// The returned span has been removed from the
// free structures, but its state is still mSpanFree.
func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan {
	// 從堆中獲取 free 的 span
	t := h.free.find(npage)
	if t.valid() {
		goto HaveSpan
	}
	// 堆中沒無法獲取到 span,這時需要對堆進行增長
	if !h.grow(npage) {
		return nil
	}
	// 再獲取一次
	t = h.free.find(npage)
	if t.valid() {
		goto HaveSpan
	}
	throw("grew heap, but no adequate free span found")

HaveSpan:
	s := t.span()
	// 一些檢查
	//......
	return s
}

heap.grow 就是向操作系統申請, 通過 heap.sysAlloc 獲取從操作系統申請而來的內存,首先嚐試
從已經保留的 arena 中獲得內存,無法獲取到合適的內存後,纔會正式向操作系統申請,而後對其進行初始化。

微對象分配(小於16bytes)

微對象(tiny object)是指那些小於 16 byte 的對象分配,
微對象分配會將多個對象存放到一起,與小對象分配相比,過程基本類似。

if size <= maxSmallSize {
	if noscan && size < maxTinySize {
		// 偏移量
		off := c.tinyoffset
		// 將微型指針對齊以進行所需(保守)對齊。
		if size&7 == 0 {
			off = alignUp(off, 8)
		} else if size&3 == 0 {
			off = alignUp(off, 4)
		} else if size&1 == 0 {
			off = alignUp(off, 2)
		}
		if off+size <= maxTinySize && c.tiny != 0 {
			// 能直接被當前的內存塊容納
			x = unsafe.Pointer(c.tiny + off)
			// 增加 offset
			c.tinyoffset = off + size
			// 統計數量
			c.local_tinyallocs++
			// 完成分配,釋放 m
			mp.mallocing = 0
			releasem(mp)
			return x
		}
		// 根據 tinySpan 的大小等級獲得對應的 span 鏈表
		// 從而用於分配一個新的 maxTinySize 塊,與小對象分配的過程一致
		span := c.alloc[tinySpanClass]
		v := nextFreeFast(span)
		if v == 0 {
			v, _, shouldhelpgc = c.nextFree(tinySpanClass)
		}
		x = unsafe.Pointer(v)
		(*[2]uint64)(x)[0] = 0
		(*[2]uint64)(x)[1] = 0
		// 看看我們是否需要根據剩餘可用空間量替換現有的小塊
		if size < c.tinyoffset || c.tiny == 0 {
			c.tiny = uintptr(x)
			c.tinyoffset = size
		}
		size = maxTinySize
	}
}

小對象分配

// 計算 size class
var sizeclass uint8
if size <= smallSizeMax-8 {
	sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
} else {
	sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
}
size = uintptr(class_to_size[sizeclass])
spc := makeSpanClass(sizeclass, noscan)
span := c.alloc[spc]
// 獲得對應 size 的 span 列表
v := nextFreeFast(span)
if v == 0 {
	v, span, shouldhelpgc = c.nextFree(spc)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
	memclrNoHeapPointers(unsafe.Pointer(v), size)
}

實際上基於 nextFreeFastnextFree 兩個分配調用隱藏了相當複雜的過程。

nextFreeFast 不涉及正式的分配過程,只是簡單的尋找一個能夠容納當前微型對象的 span:

// nextFreeFast returns the next free object if one is quickly available.
// Otherwise it returns 0.
func nextFreeFast(s *mspan) gclinkptr {
	// 檢查莫爲零的個數
	theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache?
	// 如果小於 64 則說明可以直接使用
	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)
			s.freeindex = freeidx
			s.allocCount++
			return gclinkptr(result*s.elemsize + s.base())
		}
	}
	return 0
}

allocCache 這裏是用位圖表示內存是否可用,1 表示可用。allocCache 字段用於計算 freeindex 上的 allocBits 緩存,allocCache 進行了移位使其最低位對應於freeindex 位。allocCache 保存 allocBits 的補碼,從而尾零計數可以直接使用它。

如果 mcache.alloc[sizeclass] 已經不夠用了,則從 mcentral 申請內存到 mcache。

func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
	s = c.alloc[spc]
	// 獲得 s.freeindex 中或之後 s 中下一個空閒對象的索引
	freeIndex := s.nextFreeIndex()
	if freeIndex == s.nelems {
		// span 已滿,進行填充
		......
		// 這個地方 mcache 向 mcentral 申請
		c.refill(spc)
		shouldhelpgc = true
		s = c.alloc[spc]
		// mcache 向 mcentral 申請完之後,再次從 mcache 申請
		freeIndex = s.nextFreeIndex()
	}
	......
	v = gclinkptr(freeIndex*s.elemsize + s.base())
	s.allocCount++
	......
	return
}

當 span 已滿時候,會通過 refill 進行填充,而後再次嘗試獲取 freeIndex。可以看到 refill 其實是從 mcentral 調用 cacheSpan 方法來獲得 span:

func (c *mcache) refill(spc spanClass) {
	_g_ := getg()

	_g_.m.locks++
	// Return the current cached span to the central lists.
	s := c.alloc[spc]

	(...)
	// Get a new cached span from the central lists.
	s = mheap_.central[spc].mcentral.cacheSpan()
	if s == nil {
		throw("out of memory")
	}
	(...)
	c.alloc[spc] = s
}

// Allocate a span to use in an MCache.
func (c *mcentral) cacheSpan() *mspan {
    ...
    // Replenish central list if empty.
    s = c.grow()
}

func (c *mcentral) grow() *mspan {
    npages := uintptr(class_to_allocnpages[c.sizeclass])
    size := uintptr(class_to_size[c.sizeclass])
    n := (npages << _PageShift) / size
    
    //這裏想 mheap 申請
    s := mheap_.alloc(npages, c.sizeclass, false, true)
    ...
    return s
}

如果 mheap 不足,則想 OS 申請。接上面的代碼 mheap_.alloc(), 細節已經在大對象的分配過程中討論過了,這裏便不再描述。

總結:

  • 類似於TCMalloc的結構
  • 使用span機制來減少碎片. 每個span至少爲一個頁(go中的一個page爲8KB). 每一種span用於一個範圍的內存分配需求. 比如16-32byte使用分配32byte的span, 112-128使用分配128byte的span.
  • 一共有67個size範圍, 8byte-32KB, 每個size有兩種類型(scan和noscan, 表示分配的對象是否會包含指針)
  • 多階Cache來減少分配的衝突. per-P無鎖的mcache, 對應不同size(67*2)的全局mcentral, 全局的mheap.
  • go代碼分配內存優先從當前p的mcache對應size的span中獲取; 有的話, 再從對應size的mcentral中獲取一個span; 還沒有的話, 從mheap中sweep一個span; sweep不出來, 則從mheap中空閒塊找到對應span大小的內存. mheap中如果還沒有, 則從系統申請內存. 從無鎖到全局1/(67*2)粒度的鎖, 再到全局鎖, 再到系統調用.
  • stack的分配也是多層次和多class的. 減少分配的鎖爭搶, 減少棧浪費.
  • mheap中以treap的結構維護空閒連續page. 歸還內存到mheap時, 連續地址會進行合併. (1.11之前採用類似夥伴系統維護<1MB的連續page, treap維護>1MB的連續page)
  • 對象由GC進行釋放. sysmon會定時把mheap空餘的內存歸還給操作系統
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章