「Golang 系列」 Golang 內存管理和回收

「Golang 系列」 Golang 內存管理和回收

本文基於Go 1.13

當不再使用內存時,標準庫會自動執行Go內存管理,即從內存分配到其集合。儘管開發人員不必處理它,但是Go進行的基礎管理已得到了很好的優化,並且充滿了有趣的概念。

堆上的分配

內存管理旨在在併發環境中快速運行,並與垃圾回收器集成在一起。讓我們從一個簡單的例子開始:

package main

type smallStruct struct {
a, b int64
c, d float64
}

func main() {
smallAllocation()
}

//go:noinline
func smallAllocation() *smallStruct {
return &smallStruct{}
}

註釋//go:noinline將禁用通過刪除函數來優化代碼的內聯,因此最終沒有分配。

運行帶有Escape Analysis命令go tool compile "-m" main.go將確認Go所做的分配:

main.go:14:9: &smallStruct literal escapes to heap

藉助,轉儲該程序的彙編代碼go tool compile -S main.go,也將向我們明確顯示分配:

0x001d 00029 (main.go:14) LEAQ type."".smallStruct(SB), AX
0x0024 00036 (main.go:14) PCDATA $0, $0
0x0024 00036 (main.go:14) MOVQ AX, (SP)
0x0028 00040 (main.go:14) CALL runtime.newobject(SB)

該函數newobject是新分配和代理的內置mallocgc函數,它是在堆上管理它們的函數。Go中有兩種策略,一種用於較小的分配,一種用於較大的分配。

小分配

對於32kb以下的小分配,Go會嘗試從名為的本地緩存中獲取內存mcache。此緩存處理一個跨度列表(32kb的內存塊),稱為mspan,其中包含可用於分配的內存:

「Golang 系列」 Golang 內存管理和回收

每個線程M都分配給一個處理器P,一次最多處理一個goroutine。在分配內存時,當前的goroutine將使用其當前的本地緩存P來查找範圍列表中可用的第一個空閒對象。使用此本地緩存不需要鎖定,並使分配效率更高。

範圍列表分為8個字節到32k字節的70個大小類別,可以存儲不同的對象大小:

「Golang 系列」 Golang 內存管理和回收

每個跨度存在兩次:一個不包含指針的對象的列表,另一個包含指針的對象的列表。這種區別將使垃圾收集器的壽命更加輕鬆,因為它不必掃描不包含任何指針的範圍。

在我們之前的示例中,結構的大小為32個字節,將適合32個字節的跨度:

「Golang 系列」 Golang 內存管理和回收

現在,我們可能想知道如果跨度在分配期間沒有空閒時隙,將會發生什麼情況。Go維護每個大小類的跨度的中央列表,稱為mcentral,其中跨度包含可用對象,而跨度包含自由對象:

「Golang 系列」 Golang 內存管理和回收

mcentral維護跨度的雙鏈表;它們每個都有對上一個跨度和下一個跨度的引用。非空列表中的跨度(“非空”表示列表中至少有一個空閒插槽可供分配)可能已經包含一些正在使用的內存。確實,當垃圾收集器掃描內存時,它可以清除跨度的一部分(標記為不再使用的那一部分),並將其放回非空列表中。

現在,我們的程序可以在沒有插槽的情況下從中央列表中請求跨度:

「Golang 系列」 Golang 內存管理和回收

如果空列表中沒有新的跨度,Go需要一種方法來將新的跨度移到中心列表。現在將從堆中分配新的範圍,並將其鏈接到中央列表:

「Golang 系列」 Golang 內存管理和回收

堆在需要時從OS中提取內存。如果需要更多的內存,堆將為arena64位體系結構分配一個稱為64Mb的大塊內存,對於其他大多數體系結構則分配為4Mb。舞臺還使用跨度映射內存頁面:

「Golang 系列」 Golang 內存管理和回收

大分配

Go不會使用本地緩存來管理大量分配。這些大於32kb的分配將四捨五入為頁面大小,並將頁面直接分配給堆。

「Golang 系列」 Golang 內存管理和回收

一圖勝千言

現在,我們可以很好地瞭解內存分配過程中正在發生的事情。讓我們將所有組件放在一起以獲得全貌:

「Golang 系列」 Golang 內存管理和回收


分享到:


相關文章: