Go:內(nèi)存管理與內(nèi)存清理

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.
這篇文章基于 Go 1.13 版本。有關(guān)內(nèi)存管理的討論在我的文章 ”Go:內(nèi)存管理與分配[1] ” 中有解釋。
清理內(nèi)存是一個(gè)過程,它能夠讓 Go 知道哪些內(nèi)存段最近可用于分配。但是,它并不會(huì)使用將位置 0 的方式來清理內(nèi)存。
將內(nèi)存置 0
將內(nèi)存置 0 的過程 —— 就是把內(nèi)存段中的所有位賦值為 0 —— 是在分配過程中即時(shí)執(zhí)行的。

Zeroing the memory
但是,我們可能想知道 Go 采用什么樣的策略去知道哪些對象能夠用于分配。由于在每個(gè)范圍內(nèi)有一個(gè)內(nèi)部位圖 allocBits,Go 實(shí)際上會(huì)追蹤那些空閑的對象。讓我們從初始態(tài)開始來回顧一下它的工作流程,

Free objects tracking with allocBits
就性能角度來看,allocBits 代表了一個(gè)初始態(tài)并且會(huì)保持不變,但是它會(huì)由 freeIndex(一個(gè)指向第一個(gè)空閑位置的增量計(jì)數(shù)器)所協(xié)助。
然后,第一個(gè)分配就開始了:

Free objects tracking with allocBits
freeIndex 現(xiàn)在增加了,并且基于 allocBits 知道了下一段空閑位置。
分配過程將會(huì)再一次出現(xiàn),之后, GC 將會(huì)啟動(dòng)去釋放不再被使用的內(nèi)存。在標(biāo)記期間,GC 會(huì)用一個(gè)位圖 gcmarkBits 來跟蹤在使用中的內(nèi)存。讓我們通過我們運(yùn)行的程序以相同的示例為例,在第一個(gè)塊不再被使用的地方。

Memory tracking during the garbage collector
正在被使用的內(nèi)存被標(biāo)記為黑色,然而當(dāng)前執(zhí)行并不能夠到達(dá)的那些內(nèi)存會(huì)保持為白色。
有關(guān)更多關(guān)于標(biāo)記和著色階段的信息,我建議你閱讀我的這篇文章 Go:GC 是如何標(biāo)記內(nèi)存的?[2]
現(xiàn)在,我們可以使用 gomarkBits 精確查看可用于分配的內(nèi)存。Go 現(xiàn)在也使用 gomarkBits 代替了 allocBits ,這個(gè)操作就是內(nèi)存清理:

Sweeping a span
但是,這必須在每一個(gè)范圍內(nèi)執(zhí)行完畢并且會(huì)花費(fèi)許多時(shí)間。Go 的目標(biāo)是在清理內(nèi)存時(shí)不阻礙執(zhí)行,并為此提供了兩種策略。
清理階段
Go 提供了兩種方式來清理內(nèi)存:
使用一個(gè)工作程序在后臺等待,一個(gè)一個(gè)的清理這些范圍。 當(dāng)分配需要一個(gè)范圍的時(shí)候即時(shí)執(zhí)行。
關(guān)于后臺工作程序,當(dāng)開始運(yùn)行程序時(shí),Go 將設(shè)置一個(gè)后臺運(yùn)行的 Worker(唯一的任務(wù)就是去清理內(nèi)存),它將進(jìn)入睡眠狀態(tài)并等待內(nèi)存段掃描:

Background sweeper
通過追蹤過程的周期,我們也能看到這個(gè)后臺工作程序總是出現(xiàn)去清理內(nèi)存:

Background sweeper
清理內(nèi)存段的第二種方式是即時(shí)執(zhí)行。但是,由于這些內(nèi)存段已經(jīng)被分發(fā)到每一個(gè)處理器的本地緩存 mcache 中,因此很難追蹤首先清理哪些內(nèi)存。這就是為什么 Go 首先將所有內(nèi)存段移動(dòng)到 mcentral 的原因。

Spans are released to the central list
然后,它將會(huì)讓本地緩存 mcache 再次請求它們,去即時(shí)清理:

Sweep span on the fly during allocation
即時(shí)掃描確保所有內(nèi)存段在保存資源的過程中都會(huì)得到清理,同時(shí)會(huì)保存資源以及不會(huì)阻塞程序執(zhí)行。
與 GC 周期的沖突
正如之前看到的,由于后臺只有一個(gè) worker 在清理內(nèi)存塊,清理過程可能會(huì)花費(fèi)一些時(shí)間。但是,我們可能想知道如果另一個(gè) GC 周期在一次清理過程中啟動(dòng)會(huì)發(fā)生什么。在這種情況下,這個(gè)運(yùn)行 GC 的 Goroutine 就會(huì)在開始標(biāo)記階段前去協(xié)助完成剩余的清理工作。讓我們舉個(gè)例子看一下連續(xù)調(diào)用兩次 GC,包含數(shù)千個(gè)對象的內(nèi)存分配的過程。

Sweeping must be finished before a new cycle
但是,如果開發(fā)者沒有強(qiáng)制調(diào)用 GC,這個(gè)情況并不會(huì)發(fā)生。在后臺運(yùn)行的清理工作以及在執(zhí)行過程中的清理工作應(yīng)該足夠多,因?yàn)榍謇韮?nèi)存塊的數(shù)量和去觸發(fā)一個(gè)新的周期(譯者注:GC 周期)的所需的分配的數(shù)量成正比。via:https://medium.com/a-journey-with-go/go-memory-management-and-memory-sweep-cc71b484de05
作者:Vincent Blanchon[3]譯者:sh1luo[4]校對:polaris1119[5]
本文由 GCTT[6] 原創(chuàng)編譯,Go 中文網(wǎng)[7] 榮譽(yù)推出
參考資料
Go:內(nèi)存管理與分配: https://studygolang.com/articles/28436
[2]Go:GC 是如何標(biāo)記內(nèi)存的?: https://studygolang.com/articles/25916
[3]Vincent Blanchon: https://medium.com/@blanchon.vincent
[4]sh1luo: https://github.com/sh1luo
[5]polaris1119: https://github.com/polaris1119
[6]GCTT: https://github.com/studygolang/GCTT
[7]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
