Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.
這篇文章基於 Go 1.13 版本。有關記憶體管理的討論在我的文章 ”Go:記憶體管理與分配 ” 中有解釋。
清理記憶體是一個過程,它能夠讓 Go 知道哪些記憶體段最近可用於分配。但是,它並不會使用將位置 0 的方式來清理記憶體。
將記憶體置 0
將記憶體置 0 的過程 —— 就是把記憶體段中的所有位賦值為 0 —— 是在分配過程中即時執行的。
Zeroing the memory
但是,我們可能想知道 Go 採用什麼樣的策略去知道哪些物件能夠用於分配。由於在每個範圍內有一個內部點陣圖 allocBits
,Go 實際上會追蹤那些空閒的物件。讓我們從初始態開始來回顧一下它的工作流程,
Free objects tracking with allocBits
就效能角度來看,allocBits
代表了一個初始態並且會保持不變,但是它會由 freeIndex
(一個指向第一個空閒位置的增量計數器)所協助。
然後,第一個分配就開始了:
Free objects tracking with allocBits
freeIndex
現在增加了,並且基於 allocBits
知道了下一段空閒位置。
分配過程將會再一次出現,之後, GC 將會啟動去釋放不再被使用的記憶體。在標記期間,GC 會用一個點陣圖 gcmarkBits
來跟蹤在使用中的記憶體。讓我們通過我們執行的程式以相同的示例為例,在第一個塊不再被使用的地方。
Memory tracking during the garbage collector
正在被使用的記憶體被標記為黑色,然而當前執行並不能夠到達的那些記憶體會保持為白色。
有關更多關於標記和著色階段的資訊,我建議你閱讀我的這篇文章 Go:GC 是如何標記記憶體的? 現在,我們可以使用
gomarkBits
精確檢視可用於分配的記憶體。Go 現在也使用gomarkBits
代替了allocBits
,這個操作就是記憶體清理:
Sweeping a span
但是,這必須在每一個範圍內執行完畢並且會花費許多時間。Go 的目標是在清理記憶體時不阻礙執行,併為此提供了兩種策略。
清理階段
Go 提供了兩種方式來清理記憶體:
- 使用一個工作程式在後臺等待,一個一個的清理這些範圍。
- 當分配需要一個範圍的時候即時執行。
關於後臺工作程式,當開始執行程式時,Go 將設定一個後臺執行的 Worker(唯一的任務就是去清理記憶體),它將進入睡眠狀態並等待記憶體段掃描:
Background sweeper
通過追蹤過程的週期,我們也能看到這個後臺工作程式總是出現去清理記憶體:
Background sweeper
清理記憶體段的第二種方式是即時執行。但是,由於這些記憶體段已經被分發到每一個處理器的本地快取 mcache
中,因此很難追蹤首先清理哪些記憶體。這就是為什麼 Go 首先將所有記憶體段移動到 mcentral
的原因。
Spans are released to the central list
然後,它將會讓本地快取 mcache
再次請求它們,去即時清理:
Sweep span on the fly during allocation
即時掃描確保所有記憶體段在儲存資源的過程中都會得到清理,同時會儲存資源以及不會阻塞程式執行。
與 GC 週期的衝突
正如之前看到的,由於後臺只有一個 worker 在清理記憶體塊,清理過程可能會花費一些時間。但是,我們可能想知道如果另一個 GC 週期在一次清理過程中啟動會發生什麼。在這種情況下,這個執行 GC 的 Goroutine 就會在開始標記階段前去協助完成剩餘的清理工作。讓我們舉個例子看一下連續呼叫兩次 GC,包含數千個物件的記憶體分配的過程。
Sweeping must be finished before a new cycle
但是,如果開發者沒有強制呼叫 GC,這個情況並不會發生。在後臺執行的清理工作以及在執行過程中的清理工作應該足夠多,因為清理記憶體塊的數量和去觸發一個新的週期(譯者注:GC 週期)的所需的分配的數量成正比。
作者:Vincent Blanchon 譯者:sh1luo 校對:polaris1119