golang源碼解析--內存總覽

看了gc,發現沒有內存的知識,光看gc,只能背個流程,其中很多涉及內存的操作,所以先來了解一波內存,然後瞭解gc的協程,golang的鎖,再回頭看gc

golang內存分配的簡介

關於golang的內存分配

原理:
思想來源於Thread-CachingMalloc。核心思想就是把內存分爲多級管理,從而降低鎖的粒度。它將可用的堆內存採用二級分配的方式進行管理:每個線程都會自行維護一個獨立的內存池,進行內存分配時優先從該內存池中分配,當內存池不足時纔會向全局內存池申請,以避免不同線程對全局內存池的頻繁競爭。

內存分配工作通過內存頁來進行。
小的內存分配(0~32kb)被分成了70個級別,每一個都有它自己的一組完全相同大小的對象。任何可用內存頁都可以拆分爲一組對象,然後使用可用位圖管理這些對象。

相關的數據結構

fixalloc:固定大小堆外對象的空閒列表分配器,用於管理分配器使用的存儲。
mheap:malloc堆,按頁(8192字節)粒度管理。
mspan:由mheap管理的一系列頁面。
mcentral:收集給定大小類的所有span
mcache: 一個P的可用內存空間
mstats: 分配統計信息

golang如何分配一個小對象

分配一個小對象將沿着緩存的層次結構前進
1.將小對象的大小,四捨五入的(保證class的大小大於對象內存)分配在一個小類中,並在P的mcache中查詢對應的mspan,掃描mcache中的mspan的空餘位去找可用的slot。如果存在可用slot,分配內存。這個過程的完成不需要鎖。
2.如果申請的mspan在mcache中沒有可用插槽,請從mcentral的具有可用空間的所需大小類別的mspan列表中獲取一個新的mspan。獲得整個span會攤銷鎖定mcentral的成本。
3.如果mcentral的mspan列表爲空,請從mheap獲取一系列用於mspan的頁面。
4.若mheap爲空,或者沒有足夠大的頁,從操作系統申請一組新的頁(至少1MB),分配大量的頁面可以分攤與操作系統對話的成本。

golang如何釋放mspan上的內存

掃掠mspan並釋放其上的對象將沿着類似的層次結構進行:
1.如果mspan是響應於分配而被回收的,那麼它將被返回到mcache以滿足分配。
2.非情況1,如果msapn中仍有分配的對象,則它被放在mcentral的mspan的size類的空餘列表中
3.非1,2,如果所有的對象在mspan中都是空閒的,則mspan是空閒的,則會返回mheap並不再有size class的屬性。這可能會與相鄰的空閒mspan合併。
4.如果mspan保持空閒狀態足夠長的時間,則將其頁面返回到操作系統。

golang如何分配和釋放一個大對象

分配和釋放大對象直接使用mheap,而繞過mcache和mcentral。

延遲歸零機制

僅當mspan.needzero爲false時,纔會將mspan中的可用對象插槽清零。
如果needzero爲true,則在分配對象時將其清零。
延遲調零有多種好處:
1.堆棧幀分配可以完全避免歸零。
2.由於程序可能即將寫入內存,因此它展現了更好的時間局部性。
3.我們不會將永遠不會被重用的頁面歸零。

虛擬內存層

堆由一組arena組成,在64位計算機上是64MB,在32位計算機上是4MB,每個arena的起始地址也與arena大小對齊
每個arena都有一個關聯的heapArena對象,該對象存儲該arena的元數據:arena中所有單詞的堆位圖和arena中所有頁面的跨度圖。 它們本身是堆外分配的。
由於arena是對齊的,因此可以將地址空間視爲一系列arena框架。arena映射(mheap_.arenas)從arena幀號映射到* heapArena,對於不由Go堆支持的部分地址空間,映射爲nil。 arena圖(map)的結構爲兩層數組,由“ L1”arena圖和許多“ L2”arena圖組成。 但是,由於arena很大,因此在許多體系結構上,arena map都由一個單獨的大型L2 map組成。
arena map覆蓋了整個可能的地址空間,從而允許Go堆使用地址空間的任何部分。 分配器嘗試使arena保持連續,以便大跨度(以及大對象)可以跨越arena。

golang關於內存分配的基礎概念

這一部分內容源自http://www.sohu.com/a/300983903_657921
在這裏插入圖片描述
arena區域就是我們所謂的堆區,Go動態分配的內存都是在這個區域,它把內存分割成 8KB大小的頁,一些頁組合起來稱爲 mspan。
bitmap區域標識 arena區域哪些地址保存了對象,並用 4bit標誌位表示對象是否包含指針、 GC標記信息。 bitmap中一個 byte大小的內存對應 arena區域中4個指針大小(指針大小爲 8B )的內存,所以 bitmap區域的大小是 512GB/(4x8B)=16GB。
spans區域存放 mspan(也就是一些 arena分割的頁組合起來的內存管理基本單元,後文會再講)的指針,每個指針對應一頁,所以 spans區域的大小就是 512GB/8KBx8B=512MB。除以8KB是計算 arena區域的頁數,而最後乘以8是計算 spans區域所有指針的大小。創建 mspan的時候,按頁填充對應的 spans區域,在回收 object時,根據地址很容易就能找到它所屬的 mspan。
總結
Go在程序啓動時,會向操作系統申請一大塊內存,之後自行管理。
Go內存管理的基本單元是mspan,它由若干個頁組成,每種mspan可以分配特定大小的object。
mcache, mcentral, mheap是Go內存管理的三大組件,層層遞進。mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供所有線程使用;mheap管理Go的所有動態分配內存。
極小對象會分配在一個object中,以節省資源,使用tiny分配器分配內存;一般小對象通過mspan分配內存;大對象則直接由mheap分配內存。
概念:
mspan:Go中內存管理的基本單元,是由一片連續的 8KB的頁組成的大塊內存。注意,這裏的頁和操作系統本身的頁並不是一回事,它一般是操作系統頁大小的幾倍。一句話概括: mspan是一個包含起始地址、 mspan規格、頁的數量等內容的雙端鏈表。
內存管理組件

內存分配由內存分配器完成。分配器由3種組件構成: mcache, mcentral, mheap。
mcache

mcache:每個工作線程都會綁定一個mcache,本地緩存可用的 mspan資源,這樣就可以直接給Goroutine分配,因爲不存在多個Goroutine競爭的情況,所以不會消耗鎖資源。
mcentral:爲所有 mcache提供切分好的 mspan資源。每個 central保存一種特定大小的全局 mspan列表,包括已分配出去的和未分配出去的。 每個 mcentral對應一種 mspan,而 mspan的種類導致它分割的 object大小不同。當工作線程的 mcache中沒有合適(也就是特定大小的)的 mspan時就會從 mcentral獲取。
mheap:代表Go程序持有的所有堆空間,Go程序使用一個 mheap的全局對象 _mheap來管理堆內存。
當 mcentral沒有空閒的 mspan時,會向 mheap申請。而 mheap沒有資源時,會向操作系統申請新內存。 mheap主要用於大對象的內存分配,以及管理未切割的 mspan,用於給 mcentral切割成小對象。
同時我們也看到, mheap中含有所有規格的 mcentral,所以,當一個 mcache從 mcentral申請 mspan時,只需要在獨立的 mcentral中使用鎖,並不會影響申請其他規格的 mspan。

源碼分析

golang內存分配的過程

mallocgc

//分配一個大小爲字節的對象。
//從per-P緩存的空閒列表中分配小對象。
//從堆直接分配大對象(> 32 kB)。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	//目前gc還處於stop the world階段,禁止申請
	if gcphase == _GCmarktermination {
		throw("mallocgc called with gcphase == _GCmarktermination")
	}
	//分配空指針
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
	//如果debug.sbrk != 0則不會釋放內存 
	if debug.sbrk != 0 {
		align := uintptr(16)
		if typ != nil {
			align = uintptr(typ.align)
		}
		//分配持久內存
		return persistentalloc(size, align, &memstats.other_sys)
	}

	// assistG 是這次分配內存的G,如果GC當前非活躍,則爲空
	var assistG *g
	if gcBlackenEnabled != 0 {
	    // gc處於標記狀態
		// 從當前G分撇內存
		assistG = getg()
		if assistG.m.curg != nil {
			assistG = assistG.m.curg
		}
		// 從當前g獲取內存
		assistG.gcAssistBytes -= int64(size)

		if assistG.gcAssistBytes < 0 {
			//這個G沒有足夠內存。 協助GC進行更正,然後再分配。 這必須在禁用搶佔之前發生。
			gcAssistAlloc(assistG)
		}
	}

	// Set mp.mallocing to keep from being preempted by GC.
	// 搶佔鎖防止GC使用
	mp := acquirem()
	if mp.mallocing != 0 {
		throw("malloc deadlock")
	}
	if mp.gsignal == getg() {
		throw("malloc during signal")
	}
	mp.mallocing = 1

	shouldhelpgc := false
	dataSize := size
	c := gomcache()
	var x unsafe.Pointer
	noscan := typ == nil || typ.kind&kindNoPointers != 0
	if size <= maxSmallSize {
		if noscan && size < maxTinySize {
	     //針對微小分配器的分配
	     //微小的內存分配將多個微小內存分配的請求分配到一個內存塊上。當所有的子對象都不可達,釋放該內存塊。
		//所有子對象不能有指針,用於限制可能浪費的內存

        //用於合併的存儲塊的大小是可調的
		//當前設置是16bytes,這個的標準是與最壞情況下浪費內存的兩倍有關(當僅有一個子對象可達,其餘對象都不可達)
		//8bytes將沒有內存浪費,但是對於組合請求的概率將大大減少
		//32bytes會讓更多的請求結合在一個內存塊,但是會造成4倍的內存浪費
		//無論塊大小多少,最好的案例都是8的倍數

       //從微小分配器分配的內容都不能被顯示釋放,所以我們要確保當釋放內存的時候它的大小大於maxTinySize(默認16bytes)
      //SetFinalizer針對由微小分配器分配的對象有特殊處理方式,
	  //可以爲內存塊的內部字節設置終結器  
      // 微小分配器主要針對小字符串和獨立的轉義變量
	// 在json基礎上,分配器將內存消耗的數量減少了12%,並將hepsize減少了20%
            // 對其微小指針 
			if size&7 == 0 {
				off = round(off, 8)
			} else if size&3 == 0 {
				off = round(off, 4)
			} else if size&1 == 0 {
				off = round(off, 2)
			}
			if off+size <= maxTinySize && c.tiny != 0 {
				// The object fits into existing tiny block.
				//已有的微小塊有適合對象的存在,則直接分配完畢
				x = unsafe.Pointer(c.tiny + off)
				c.tinyoffset = off + size
				c.local_tinyallocs++
				mp.mallocing = 0
				releasem(mp)
				return x
			}
			// 分配一個新的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
			// See if we need to replace the existing tiny block with the new one
			// based on amount of remaining free space.
			if size < c.tinyoffset || c.tiny == 0 {
				c.tiny = uintptr(x)
				c.tinyoffset = size
			}
			size = maxTinySize
		} else {
		     //非微小對象的情況,內存消耗大於16字節
			//先確定對象屬於的類,直接申請
			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]
			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)
			}
		}
	} else {
	//申請大對象,大於32kb
		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
	}

	var scanSize uintptr
	if !noscan {
	     //若申請的是defer+arg的塊,我們已經針對此獲取了足夠大的內存,將"asked for"的大小
		//削減至defer頭的大小,爲的是GC bitmap可以將arg塊記錄爲什麼也沒有(如同將其記錄爲一個內存塊由於size對齊而未使用的部分)
		//defer arg部分將被當做scanstak的一部分被回收
		if typ == deferType {
			dataSize = unsafe.Sizeof(_defer{})
		}
		heapBitsSetType(uintptr(x), size, dataSize, typ)
		if dataSize > typ.size {
			// Array allocation. If there are any
			// pointers, GC has to scan to the last
			// element.
			if typ.ptrdata != 0 {
				scanSize = dataSize - typ.size + typ.ptrdata
			}
		} else {
			scanSize = typ.ptrdata
		}
		c.local_scan += scanSize
	}

	//將申請的內存在被垃圾收集器可觀察前初始化爲類型安全的內存,並設置堆位的存儲
	//目的是防止垃圾收集器跟隨指向x的指針看到未初始化的或陳舊的堆棧
	publicationBarrier()

	//GC期間,新申請內存爲黑色
	//所有slot爲空,則不需要清掃
	//這可能與gc產生競爭,所以自動標記該位若可能存在競爭
	if gcphase != _GCoff {
		gcmarknewobject(uintptr(x), size, scanSize)
	}

	if raceenabled {
		racemalloc(x, size)
	}

	if msanenabled {
		msanmalloc(x, size)
	}

	mp.mallocing = 0
	releasem(mp)

	if debug.allocfreetrace != 0 {
		tracealloc(x, size, typ)
	}

	if rate := MemProfileRate; rate > 0 {
		if rate != 1 && int32(size) < c.next_sample {
			c.next_sample -= int32(size)
		} else {
			mp := acquirem()
			profilealloc(mp, x, size)
			releasem(mp)
		}
	}

	if assistG != nil {
		// Account for internal fragmentation in the assist
		// debt now that we know it.
		assistG.gcAssistBytes -= int64(size - dataSize)
	}
   //若申請大內存,則判斷是否需要進行內存回收也就是gc操作
	if shouldhelpgc {
		if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
			gcStart(t)
		}
	}

	return x
}
發佈了210 篇原創文章 · 獲贊 33 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章