進程里面的堆和棧
我們知道進程之間內存是隔離的不共享。所以一般說到內存就是指的一個進程用到的內存。
而一個進程的內存一般可以分為 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_size
,size_to_class
和class_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