深刻學習golang,必需要了解內存這塊,此次會仔細講解下內存這塊,包括內存分配,內存模型,逃逸分析。讓咱們在編程中能注意下這塊。golang
經過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的塊
}
複製代碼
在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
}
複製代碼
當mcache不夠時候,會向mcentral申請內存。該結構其實是在mheap中的,因此在我看來,這起到橋樑的做用。緩存
type mcentral struct {
lock mutex // 多個P會訪問,須要加鎖
spanclass spanClass // 對應了mspan中的spanclass
nonempty mSpanList // 該mcentral可用的mspan列表
empty mSpanList // 該mcentral中已經被使用的mspan列表
}
複製代碼
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分配器
}
複製代碼
接下來請細看下圖,結合前面的講解進行理解(務必看懂)。數據結構
這裏不展開源代碼,知道分配規則便可。(在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, P1中導入了包P2,則P2中的init函數Happens BeforeP1中全部的操做
2, 全部的init函數Happens Before Main函數
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完成操做。 。
爲何要作逃逸分析呢,由於在棧上分配的代價要遠小於在堆上進行分配,這塊是目前不少人缺少的一個思惟,包括我。最近看了一些這方面的文章,再回去看本身的代碼,發現不少不合理的地方,但願經過此次講解,能一塊兒進步。
簡單來講就是本來應在棧上分配內存的對象,逃逸到了堆上進行分配。若是能在棧上進行分配,那麼只須要兩個指令,入棧和出棧,GC壓力也小了。因此相比之下,在棧上分配代價會小不少。
我的總結了一下,若是沒法在編譯期肯定變量的做用域和佔用內存大小,則會逃逸到堆上。
咱們平時會知道,傳遞指針能夠減小底層值的拷貝,能夠提升效率,在通常狀況下是如此,可是若是拷貝的是少許的數據,那麼傳遞指針效率不必定會高於值拷貝。
(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。
若是編譯期沒法肯定切片的大小或者切片大小過大,超出棧大小限制,或者在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
複製代碼
可見,咱們不必定是必定要用切片代替數組,由於切片底層數組可能會在堆上分配內存,並且小數組在棧上拷貝的消耗也未必比切片大。
interface是咱們在go中常常會用到的特性,很是好用,可是因爲interface類型在編譯期間,編譯期很難肯定其具體類型,所以也致使了逃逸現象。舉個最簡單的例子
func main() {
s := "abc"
fmt.Println(s)
}
複製代碼
上述代碼會產生逃逸,緣由是fmt.Println這個方法接收的參數是interface類型。可是這塊只是做爲科普,畢竟interface帶來的好處要大於它這個缺陷