go的內存管理(一)


進程里面的堆和棧

我們知道進程之間內存是隔離的不共享。所以一般說到內存就是指的一個進程用到的內存。

而一個進程的內存一般可以分為 5個區:棧區, 堆區, 靜態區(全局區), 文字常量區,代碼區。而我們主要理解棧區和堆區,其他3個區里面的內容都是靜態的。

棧區:

函數里面涉及到幾乎大部分內容都在棧區,比如函數的實參,局部變量,操作符。

優點: cpu處理簡單速度快,函數返回,棧區里面的空間就釋放,而且對應線程是唯一的(並發安全)。

缺點: 數據結構導致操作不靈活,生命周期短;一般在編譯期間就決定了棧區的大小,通常很小。

堆區:

比較靈活的內存區,進程里面的所有線程共享。程序員操作方便,比如C語言里面malloc申請內存,free釋放內存。

優點: 用戶態程序操作方便;空間大可以申請比較大的類型數據;里面的變量生命周期長。

缺點: 進程里面所有線程共享這個區域,不是線程(並發)安全的;隨着線程不斷地申請和釋放,導致出現很多內存碎片數據區域不連續,最終導致數據的讀寫變慢,出現性能問題;沒有gc的情況下,需要自己手動管理,如果管理不當很容易造成OOM。

 

由於堆區的靈活性還有不安全,使得我們不得對其進行管理,比如GC;如果想用的更高效,就得最好還可以管理堆區的分配。所以我們所說的內存管理,主要就是管理堆區的內存分配和釋放。

 在放一張go內存管理的宏觀圖加深理解:

TCMalloc

全稱Thread Cache Malloc,是google開源的內存管理庫。其實有很多內存管理庫,但他們追求的本質是在多線程編程下,追求更高內存管理效率。

Go的內存管理是借鑒了TCMalloc,隨着Go的迭代,Go的內存管理與TCMalloc不一致地方在不斷擴大,但其主要思想、原理和概念都是和TCMalloc一致的。

TCMalloc的細節這里不作講述。詳情介紹可以參考下面兩篇文章:

TCMalloc(英文):     http://goog-perftools.sourceforge.net/doc/tcmalloc.html

TCMalloc介紹(中文):   https://blog.csdn.net/aaronjzhang/article/details/8696212

 

Go內存管理

Go內存管理源自TCMalloc,但它比TCMalloc還多了2件東西:逃逸分析垃圾回收。逃逸分析和GC會在后面的文章中分享。

再看一張Go內存管理各個模塊配合工作的圖片:

 

 咱們先簡單了解一下go內存管理的工作流程。

簡單流程

我們的go進程需要申請一個小對象(<=32KB)的時候直接從mcache里面申請,如果mcache里面沒有多余的空間分配,就向mcentral申請一個單位的空間(xKB,具體大小先不管,后面會說)。如果mcentral沒有多余的呢,就向mheap申請;如果mheap也不夠了呢,mheap就直接從操作系統中分配一組新的內存空間(至少1MB)。

如果申請的大對象(>32KB),直接從mheap分配。

可以發現流程很簡單,就是當自己需要內存就向上一級申請內存空間,如果沒有多余,就自己上級模塊再向他的上一級的內存模塊申請空間,依次類推直到內核。

核心思想

把內存分為多級管理,降低鎖的粒度(只是去mcentral和mheap會申請鎖), 以及多種對象大小類型,減少分配產生的內存碎片。

接下來就詳細說一些模塊和概念。

 

重要概念 

Page

操作系統內存管理中,內存的最粒度是4KB,也就是說分配內存最小4kb起。而golang里面一個page是8KB。

Span

Span是內存管理的基本單位,代碼中為mspan,一組連續的Page組成1個Span。mspan其實是一個雙向鏈表的結構,其中包含頁面的起始地址,它具有的頁面的span類以及它包含的頁面數(npage)。后面我們會細說mspan的對象結構,以及什么是span class。

type mspan struct {
	next *mspan     // next span in list, or nil if none
	prev *mspan     // previous span in list, or nil if none
	list *mSpanList // For debugging. TODO: Remove.

	startAddr uintptr // address of first byte of span aka s.base()
	npages    uintptr // number of pages in span
.....

  

mcache

mcache保存的是各種大小的Span,並按Span class分類,小對象(<=32KB)直接從mcache分配內存,它起到了緩存的作用,並且可以無鎖訪問

mcache是每個邏輯處理器(P)的本地內存線程緩存。Go中是每個P擁有1個mcache。

mcache中每個級別的Span有2類數組鏈表但是合在一起的(alloc成員變量)。這和mcache申請內存有關,稍后我們再解釋。

type mcache struct {
	// The following members are accessed on every malloc,
	// so they are grouped here for better caching.
	next_sample uintptr // trigger heap sample after allocating this many bytes
	local_scan  uintptr // bytes of scannable heap allocated

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

.....

  

mcentral

它按Span class對Span分類,串聯成鏈表,當mcache的某個級別Span的內存被分配光時,它會向mcentral申請1個當前級別的Span。所有線程共享的緩存,需要加鎖訪問。

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包含兩個mspanList

  • empty:雙向span鏈表,包括沒有空閑對象的span或緩存mcache中的span。當此處的span被釋放時,它將被移至non-empty span鏈表。
  • non-empty:有空閑對象的span雙向鏈表。當從mcentral請求新的span,mcentral將從該鏈表中獲取span並將其移入empty span鏈表。

mheap

它把從OS申請出的內存頁組織成Span,並保存起來。當mcentral的Span不夠用時會向mheap申請,mheap的Span不夠用時會向OS申請,向OS的內存申請是按頁來的,然后把申請來的內存頁生成Span組織起來,同樣也是需要加鎖訪問的。大對象(>32KB)直接從mheap上分配。

type mheap struct {
	// lock must only be acquired on the system stack, otherwise a g
	// could self-deadlock if its stack grows with the lock held.
	lock      mutex
	free      mTreap // free spans
	sweepgen  uint32 // sweep generation, see comment in mspan
	sweepdone uint32 // all spans are swept
	sweepers  uint32 // number of active sweepone calls

	// allspans is a slice of all mspans ever created. Each mspan
	// appears exactly once.
	//
	// The memory for allspans is manually managed and can be
	// reallocated and move as the heap grows.
	//
	// In general, allspans is protected by mheap_.lock, which
	// prevents concurrent access as well as freeing the backing
	// store. Accesses during STW might not hold the lock, but
	// must ensure that allocation cannot happen around the
	// access (since that may free the backing store).
	allspans []*mspan // all spans out there
...
    
        central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}

...

 

我們用一張圖來深化一下各個結構體之間的關系

  

 

數據大小的轉換

通過下圖看看數據大小類別之間的轉換

 

1. object size:指申請一個對象占用的內存大小。

2. size class: 簡稱class,是指size的級別,一共有67個級別。相當於把size歸類到一定大小的區間段,比如size[1,8]屬於size class 1,size(8,16]屬於size class 2。(簡單點理解就是不同的class, mspan里面npage成員變量的值就不一樣)

3. span class: 指span的級別,但span class的大小與span的大小並沒有正比關系。span class主要用來和size class做對應,1個size class對應2個span class,2個span class的span大小相同,只是功能不同,1個用來存放包含指針的對象,一個用來存放不包含指針的對象,不包含指針對象的Span就無需GC掃描了。

4. num of page:就是mspan結構體里面的npages,代表Page的數量,其實就是Span包含的頁數,用來分配內存。

再結合一下數據大小轉換表(源代碼里面有),對大小轉換加深理解。

該圖里面的第一二行對應上圖的大小類型。class_to_sizesize_to_classclass_to_allocnpages3個數組,對應內存大小轉換那幅圖上的3個箭頭。

從上圖第四行看起,看到數據一共有66行,也就是有66個class。上文不是說有67個嗎?因為還有一共是0,就沒有列舉在里面。

舉例:第一行

// class  bytes/obj  bytes/span  objects  tail waste  max waste
//     1          8        8192     1024           0     87.50%

就是類別1的對象大小是8bytes,所以class_to_size[1]=8;span大小是8KB,為1頁,所以class_to_allocnpages[1]=1

  

 

 

最后一列max waste代表最大浪費的內存百分比,計算方法在源碼printComment函數中:

func printComment(w io.Writer, classes []class) {
	fmt.Fprintf(w, "// %-5s  %-9s  %-10s  %-7s  %-10s  %-9s\n", "class", "bytes/obj", "bytes/span", "objects", "tail waste", "max waste")
	prevSize := 0
	for i, c := range classes {
		if i == 0 {
			continue
		}
		spanSize := c.npages * pageSize
		objects := spanSize / c.size
		tailWaste := spanSize - c.size*(spanSize/c.size)
		maxWaste := float64((c.size-prevSize-1)*objects+tailWaste) / float64(spanSize)
		prevSize = c.size
		fmt.Fprintf(w, "// %5d  %9d  %10d  %7d  %10d  %8.2f%%\n", i, c.size, spanSize, objects, tailWaste, 100*maxWaste)
	}
	fmt.Fprintf(w, "\n")
}

Span最浪費內存的場景是:Span內的每個對象,占用的內存都是前一個class中對象的大小加1。比如class2的對象大小是9B,且只有一個,以此類推。

這樣無法占用低一級的Span,又浪費了大量空間。所以一個Span內對象空間所浪費的內存為:所有對象空間浪費的內存之和+tail waste。

 maxWaste := float64((c.size-prevSize-1)*objects+tailWaste) / float64(spanSize)

 

上文提到1個size class對應2個span class:

numSpanClasses = _NumSizeClasses << 1

numSpanClasses為span class的數量為134個。 所以在go內存管理這張圖里面,span class的下標是從0到133。每1個span class都指向1個span,也就是mcache最多有134個span。

numSpanClasses的使用在mheap和mcache結構體里面。

 

為一個對象尋找span class尋找span的過程:

以分配一個不包含指針的,大小為24Byte的對象為例。

// 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%

size class 3,它的對象大小范圍是(16,32]Byte,24Byte剛好在此區間,所以此對象的size class為3。

 size class到span class的計算如下:

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

 

所以size class 3對應的span class為:

span class = 3 << 1 | 1 = 7 

所以該對象需要的是span class 7指向的span。

另外,包含指針noscan就是false, span class為

span class = 3 << 1 | 0 = 6

  

結語

文章重要講了go內存管理的兩點:

1. 內存管理的關鍵數據結構之間的關系。

2. 對象與go最小內存管理單元之間的大小轉換關系。

后面的文章會繼續深入講解內存分配的流程

 

參考文獻

https://mp.weixin.qq.com/s/3gGbJaeuvx4klqcv34hmmw
https://tonybai.com/2020/03/10/visualizing-memory-management-in-golang
https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM