深刻理解GO語言以內存詳解

一,前言

深刻學習golang,必需要了解內存這塊,此次會仔細講解下內存這塊,包括內存分配,內存模型,逃逸分析。讓咱們在編程中能注意下這塊。golang

二,內存分配

(1) 這裏先了解四個相關數據結構

1,mspan

經過next和prev,組成一個雙向鏈表,mspan負責管理從startAddr開始的N個page的地址空間。是基本的內存分配單位。是一個管理內存的基本單位。編程

//保留重要成員變量
type mspan struct {
	next *mspan     // 鏈表中下個span
	prev *mspan     // 鏈表中上個span	startAddr uintptr // 該mspan的起始地址
	freeindex uintptr // 表示分配到第幾個塊
	npages    uintptr // 一個span中含有幾頁
	sweepgen    uint32 // GC相關
	incache     bool       // 是否被mcache佔用
	spanclass   spanClass  // 0 ~ _NumSizeClasses之間的一個值,好比,爲3,那麼這個mspan被分割成32byte的塊
}
複製代碼

2,mcache

在go中,每一個P都會被分配一個mcache,是私有的,從這裏分配內存不須要加鎖數組

type mcache struct {
	tiny             uintptr // 小對象分配器
	tinyoffset       uintptr // 小對象分配偏移
	local_tinyallocs uintptr // number of tiny allocs not counted in other stats
	alloc [numSpanClasses]*mspan // 存儲不一樣級別的mspan
}
複製代碼

3,mcentral

當mcache不夠時候,會向mcentral申請內存。該結構其實是在mheap中的,因此在我看來,這起到橋樑的做用。緩存

type mcentral struct {
	lock      mutex    // 多個P會訪問,須要加鎖
	spanclass spanClass  // 對應了mspan中的spanclass
	nonempty  mSpanList // 該mcentral可用的mspan列表
	empty     mSpanList // 該mcentral中已經被使用的mspan列表
}
複製代碼

4,mheap

mheap是真實擁有虛擬地址的,當mcentral不夠時候,會向mheap申請。bash

type mheap struct {
	lock      mutex                    // 是公有的,須要加鎖
	free      [_MaxMHeapList]mSpanList // 未分配的spanlist,好比free[3]是由包含3個 page 的 mspan 組成的鏈表	
	freelarge mTreap                   // mspan組成的鏈表,每一個mspan的 page 個數大於_MaxMHeapList
	busy      [_MaxMHeapList]mSpanList // busy lists of large spans of given length
	busylarge mSpanList                // busy lists of large spans length >= _MaxMHeapList
	allspans []*mspan                  // 全部申請過的 mspan 都會記錄在 allspans
	spans []*mspan                     // 記錄 arena 區域頁號(page number)和 mspan 的映射關係

	arena_start uintptr // arena是Golang中用於分配內存的連續虛擬地址區域,這是該區域開始的指針
	arena_used  uintptr // 已經使用的內存的指針
	arena_alloc uintptr
	arena_end   uintptr

	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte //避免僞共享(false sharing)問題
	}
	
	spanalloc             fixalloc // allocator for span*
	cachealloc            fixalloc // mcache分配器

}

複製代碼

接下來請細看下圖,結合前面的講解進行理解(務必看懂)。數據結構

(2) 內存分配細節

這裏不展開源代碼,知道分配規則便可。(在golang1.10,MacOs 10.12中,下面的32K改成64K)多線程

1, object size > 32K;則使用 mheap 直接分配。app

2,object size < 16 byte;則使用 mcache 的小對象分配器 tiny 直接分配。函數

3,object size > 16 byte && size <= 32K byte時,先在mcache申請分配。若是 mcache對應的已經沒有可用的塊,則向mcentral請求,若是mcentral也沒有可用的塊,則向mheap申請,若是 mheap 也沒有合適的span,則向操做系統申請。學習

三,內存模型

這裏說下在golang中的happen-before(假設A和B表示一個多線程的程序執行的兩個操做。若是A happens-before B,那麼A操做對內存的影響將對執行B的線程(且執行B以前)可見)

(1) Init 函數

1, P1中導入了包P2,則P2中的init函數Happens BeforeP1中全部的操做

2, 全部的init函數Happens Before Main函數

(2) Channel

1, 對一個元素的send操做Happens Before對應的receive操做

2, 對channel的close操做Happens Before receive端的收到關閉通知操做

3, 對於無緩存的Channel,對一個元素的receive 操做Happens Before對應的send完成操做

4, 對於帶緩存的Channel,假設Channel 的buffer 大小爲C,那麼對第k個元素的receive操做,Happens Before第k+C個send完成操做。 。

四,逃逸分析

爲何要作逃逸分析呢,由於在棧上分配的代價要遠小於在堆上進行分配,這塊是目前不少人缺少的一個思惟,包括我。最近看了一些這方面的文章,再回去看本身的代碼,發現不少不合理的地方,但願經過此次講解,能一塊兒進步。

(1) 什麼是內存逃逸

簡單來講就是本來應在棧上分配內存的對象,逃逸到了堆上進行分配。若是能在棧上進行分配,那麼只須要兩個指令,入棧和出棧,GC壓力也小了。因此相比之下,在棧上分配代價會小不少。

(2) 引發逃逸的狀況

我的總結了一下,若是沒法在編譯期肯定變量的做用域和佔用內存大小,則會逃逸到堆上。

1,指針

咱們平時會知道,傳遞指針能夠減小底層值的拷貝,能夠提升效率,在通常狀況下是如此,可是若是拷貝的是少許的數據,那麼傳遞指針效率不必定會高於值拷貝。

(1) 指針是間接訪址,所指向的地址大多保存在堆上,所以考慮到GC,指針不必定是高效的。看個例子

type test struct{}

func main() {
	t1 := test1()
	t2 := test2()
	println("t1", &t1, "t2", &t2)
}

func test1() test {
	t1 := test{}
	println("t1", &t1)
	return t1
}

func test2() *test {
	t2 := test{}
	println("t2", &t2)
	return &t2
}
複製代碼

運行查看逃逸狀況(禁止內聯)

go run -gcflags '-m -l' main.go
# command-line-arguments
./main.go:36:16: test1 &t1 does not escape
./main.go:43:9: &t2 escapes to heap
./main.go:41:2: moved to heap: t2
./main.go:42:16: test2 &t2 does not escape
./main.go:31:16: main &t1 does not escape
./main.go:31:27: main &t2 does not escape
t1 0xc420049f50
t2 0x10c1648
t1 0xc420049f70 t2 0xc420049f70

複製代碼

從上面能夠看出,返回指針的test2函數中的t2逃逸到堆上,等待它的將是殘忍的GC。

2,切片

若是編譯期沒法肯定切片的大小或者切片大小過大,超出棧大小限制,或者在append時候會致使從新分配內存,這時候極可能會分配到堆上。

// 切片超過棧大小
func main(){
	s := make([]byte, 1, 64 * 1024)
	_ = s
}

// 沒法肯定切片大小
func main() {
	s := make([]byte, 1, rand2.Intn(10))
	_ = s
}
複製代碼

看完上述的例子,咱們來看個有意思的例子。咱們知道,切片比數組高效,可是,確實是如此嗎?

func array() [1000]int {
	var x [1000]int
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}

func slice() []int {
	x := make([]int, 1000)
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}

func BenchmarkArray(b *testing.B) {
	for i := 0; i < b.N; i++ {
		array()
	}
}

func BenchmarkSlice(b *testing.B) {
	for i := 0; i < b.N; i++ {
		slice()
	}
}
複製代碼

運行結果以下

go test -bench . -benchmem -gcflags "-N -l -m"
BenchmarkArray-4        30000000                52.8 ns/op             0 B/op          0 allocs/op
BenchmarkSlice-4        20000000                82.4 ns/op           160 B/op          1 allocs/op
複製代碼

可見,咱們不必定是必定要用切片代替數組,由於切片底層數組可能會在堆上分配內存,並且小數組在棧上拷貝的消耗也未必比切片大。

3,interface

interface是咱們在go中常常會用到的特性,很是好用,可是因爲interface類型在編譯期間,編譯期很難肯定其具體類型,所以也致使了逃逸現象。舉個最簡單的例子

func main() {
	s := "abc"
	fmt.Println(s)
}
複製代碼

上述代碼會產生逃逸,緣由是fmt.Println這個方法接收的參數是interface類型。可是這塊只是做爲科普,畢竟interface帶來的好處要大於它這個缺陷

五,參考文獻

segment.com/blog/alloca…

相關文章
相關標籤/搜索