一,前言
深入學習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申請。
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 直接分配。
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帶來的好處要大於它這個缺陷