帶你領略Go源碼的魅力----Go內存原理詳解

一、內存分區

代碼通過預處理、編譯、彙編、連接4步後生成一個可執行程序。python

在 Windows 下,程序是一個普通的可執行文件,如下列出一個二進制可執行文件的基本狀況:程序員

經過上圖能夠得知,在沒有運行程序前,也就是說程序沒有加載到內存前,可執行程序內部已經分好三段信息,分別爲代碼區(text)、**數據區(data)未初始化數據區(bss)**3 個部分。算法

有些人直接把data和bss合起來叫作靜態區全局區數組

一、1 代碼區(text)

存放 CPU 執行的機器指令。一般代碼區是可共享的(即另外的執行程序能夠調用它),使其可共享的目的是對於頻繁被執行的程序,只須要在內存中有一份代碼便可。代碼區一般是只讀的,使其只讀的緣由是防止程序意外地修改了它的指令。另外,代碼區還規劃了局部變量的相關信息。緩存

一、2 全局初始化數據區/靜態數據區(data)

該區包含了在程序中明確被初始化的全局變量、已經初始化的靜態變量(包括全局靜態變量和局部靜態變量)和常量數據(如字符串常量)。函數

一、3 未初始化數據區(bss)

存入的是全局未初始化變量和未初始化靜態變量。未初始化數據區的數據在程序開始執行以前被內核初始化爲 0 或者空(nil)。ui

程序在加載到內存前,代碼區和全局區(data和bss)的大小就是固定的,程序運行期間不能改變。spa

而後,運行可執行程序,系統把程序加載到內存,除了根據可執行程序的信息分出代碼區(text)、數據區(data)和未初始化數據區(bss)以外,還額外增長了棧區堆區操作系統

一、4 棧區(stack)

棧是一種先進後出的內存結構,由編譯器自動分配釋放,存放函數的參數值、返回值、局部變量等。線程

在程序運行過程當中實時加載和釋放,所以,局部變量的生存週期爲申請到釋放該段棧空間。

一、5 堆區(heap)

堆是一個大容器,它的容量要遠遠大於棧,但沒有棧那樣先進後出的順序。用於動態內存分配。堆在內存中位於BSS區和棧區之間。

根據語言的不一樣,如C語言、C++語言,通常由程序員分配和釋放,若程序員不釋放,程序結束時由操做系統回收。

Go語言、Java、python等都有垃圾回收機制(GC),用來自動釋放內存。

二、 Go Runtime內存分配

Go語言內置運行時(就是Runtime),拋棄了傳統的內存分配方式,改成自主管理。這樣能夠自主地實現更好的內存使用模式,好比內存池、預分配等等。這樣,不會每次內存分配都須要進行系統調用。

Golang運行時的內存分配算法主要源自 Google 爲 C 語言開發的TCMalloc算法,全稱Thread-Caching Malloc

核心思想就是把內存分爲多級管理,從而下降鎖的粒度。它將可用的堆內存採用二級分配的方式進行管理。

每一個線程都會自行維護一個獨立的內存池,進行內存分配時優先從該內存池中分配,當內存池不足時纔會向全局內存池申請,以免不一樣線程對全局內存池的頻繁競爭。

二、1 基本策略

  • 每次從操做系統申請一大塊內存,以減小系統調用。
  • 將申請的大塊內存按照特定的大小預先的進行切分紅小塊,構成鏈表。
  • 爲對象分配內存時,只需從大小合適的鏈表提取一個小塊便可。
  • 回收對象內存時,將該小塊內存從新歸還到原鏈表,以便複用。
  • 若是閒置內存過多,則嘗試歸還部份內存給操做系統,下降總體開銷。

**注意:**內存分配器只管理內存塊,並不關心對象狀態,並且不會主動回收,垃圾回收機制在完成清理操做後,觸發內存分配器的回收操做

二、2 內存管理單元

分配器將其管理的內存塊分爲兩種:

  • span:由多個連續的頁(page [大小:8KB])組成的大塊內存。
  • object:將span按照特定大小切分紅多個小塊,每個小塊均可以存儲對象。

用途:

span 面向內部管理

object 面向對象分配

//path:Go SDK/src/runtime/malloc.go

_PageShift      = 13
_PageSize = 1 << _PageShift		//8KB
複製代碼

在基本策略中講到,Go在程序啓動的時候,會先向操做系統申請一塊內存,切成小塊後本身進行管理。

申請到的內存塊被分配了三個區域,在X64上分別是512MB,16GB,512GB大小。

**注意:**這時還只是一段虛擬的地址空間,並不會真正地分配內存

  • arena區域

    就是所謂的堆區,Go動態分配的內存都是在這個區域,它把內存分割成8KB大小的頁,一些頁組合起來稱爲mspan。

    //path:Go SDK/src/runtime/mheap.go
    
    type mspan struct {
    	next           *mspan    	// 雙向鏈表中 指向下一個
    	prev           *mspan    	// 雙向鏈表中 指向前一個
    	startAddr      uintptr   	// 起始序號
    	npages         uintptr   	// 管理的頁數
    	manualFreeList gclinkptr 	// 待分配的 object 鏈表
         nelems 		   uintptr 		// 塊個數,表示有多少個塊可供分配
         allocCount     uint16		// 已分配塊的個數
    	...
    }
    複製代碼
  • bitmap區域

    標識arena區域哪些地址保存了對象,而且用4bit標誌位表示對象是否包含指針、GC標記信息。

  • spans區域

    存放mspan的指針,每一個指針對應一頁,因此spans區域的大小就是512GB/8KB*8B=512MB。

    除以8KB是計算arena區域的頁數,而最後乘以8是計算spans區域全部指針的大小。

二、3 內存管理組件

內存分配由內存分配器完成。分配器由3種組件構成:

  • cache

    每一個運行期工做線程都會綁定一個cache,用於無鎖 object 的分配

  • central

    爲全部cache提供切分好的後備span資源

  • heap

    管理閒置span,須要時向操做系統申請內存

二、三、1 cache

cache:每一個工做線程都會綁定一個mcache,本地緩存可用的mspan資源。

這樣就能夠直接給Go Routine分配,由於不存在多個Go Routine競爭的狀況,因此不會消耗鎖資源。

mcache 的結構體定義:

//path:Go SDK/src/runtime/mcache.go

_NumSizeClasses = 67					//67
numSpanClasses = _NumSizeClasses << 1	//134

type mcache struct {
	alloc [numSpanClasses]*mspan		//以numSpanClasses 爲索引管理多個用於分配的 span
}
複製代碼

mcache用Span Classes做爲索引管理多個用於分配的mspan,它包含全部規格的mspan。

它是 _NumSizeClasses 的2倍,也就是67*2=134,爲何有一個兩倍的關係。

爲了加速以後內存回收的速度,數組裏一半的mspan中分配的對象不包含指針,另外一半則包含指針。對於無指針對象的mspan在進行垃圾回收的時候無需進一步掃描它是否引用了其餘活躍的對象。

二、三、2 central

central:爲全部mcache提供切分好的mspan資源。

每一個central保存一種特定大小的全局mspan列表,包括已分配出去的和未分配出去的。

每一個mcentral對應一種mspan,而mspan的種類致使它分割的object大小不一樣。

//path:Go SDK/src/runtime/mcentral.go

type mcentral struct {
	lock      mutex     	// 互斥鎖
	sizeclass int32     	// 規格
	nonempty  mSpanList 	// 尚有空閒object的mspan鏈表
	empty     mSpanList 	// 沒有空閒object的mspan鏈表,或者是已被mcache取走的msapn鏈表
	nmalloc   uint64    	// 已累計分配的對象個數
}
複製代碼

二、三、3 heap

heap:表明Go程序持有的全部堆空間,Go程序使用一個mheap的全局對象_mheap來管理堆內存。

當mcentral沒有空閒的mspan時,會向mheap申請。而mheap沒有資源時,會向操做系統申請新內存。mheap主要用於大對象的內存分配,以及管理未切割的mspan,用於給mcentral切割成小對象。

同時咱們也看到,mheap中含有全部規格的mcentral,因此,當一個mcache從mcentral申請mspan時,只須要在獨立的mcentral中使用鎖,並不會影響申請其餘規格的mspan。

//path:Go SDK/src/runtime/mheap.go
type mheap struct {
	lock        mutex
	spans       []*mspan // spans: 指向mspans區域,用於映射mspan和page的關係
	bitmap      uintptr  // 指向bitmap首地址,bitmap是從高地址向低地址增加的
	arena_start uintptr  // 指示arena區首地址
	arena_used  uintptr  // 指示arena區已使用地址位置
	arena_end   uintptr  // 指示arena區末地址
	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [sys.CacheLineSize-unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
	}					//每一個 central 對應一種 sizeclass
}
複製代碼

二、4 分配流程

  • 計算待分配對象的規格(size_class)
  • 從cache.alloc數組中找到規格相同的span
  • 從span.manualFreeList鏈表提取可用object
  • 若是span.manualFreeList爲空,從central獲取新的span
  • 若是central.nonempty爲空,從heap.free/freelarge獲取,並切分紅object鏈表
  • 若是heap沒有大小合適的span,向操做系統申請新的內存

二、5 釋放流程

  • 將標記爲可回收的object交還給所屬的span.freelist
  • 該span被放回central,能夠提供cache從新獲取
  • 若是span以所有回收object,將其交還給heap,以便從新分切複用
  • 按期掃描heap裏閒置的span,釋放其佔用的內存

注意:以上流程不包含大對象,它直接從heap分配和釋放

二、6 總結

Go語言的內存分配很是複雜,它的一個原則就是能複用的必定要複用。

  • Go在程序啓動時,會向操做系統申請一大塊內存,以後自行管理。
  • Go內存管理的基本單元是mspan,它由若干個頁組成,每種mspan能夠分配特定大小的object。
  • mcache, mcentral, mheap是Go內存管理的三大組件,層層遞進。mcache管理線程在本地緩存的mspan;mcentral管理全局的mspan供全部線程使用;mheap管理Go的全部動態分配內存。
  • 通常小對象經過mspan分配內存;大對象則直接由mheap分配內存。

接下來是Go語言曾經的一大黑點:垃圾回收(GC)。能夠關注咱們的公開課,法師會帶着你們一塊兒深刻了解Go語言的GC發展和機制,掃一掃二維碼,觀看公開課的直播。

相關文章
相關標籤/搜索