Go 語言的垃圾回收

Go [>= v1.5] 的新垃圾回收器是一種併發的三色標記清除回收器,這個想法最早是由 Dijkstra 在 1978 [1] 年提出的。

Go 團隊一直在密切關注並改進 Go 語言的垃圾回收器。從每 50 毫秒一次的 10 毫秒 STW 暫停到每次 GC 有兩個 500μs 的 STW 暫停,整個改進過程可以在這裏 [2] 找到。

長期從事 Go 開發,我一直對其性能感到畏懼,因此我決定深入瞭解其中的機制,比如 Go 語言爲什麼如此高效和充滿前途,瞭解它使用的是什麼樣的垃圾回收器,goroutine 如何在 OS 線程上多路複用,如何對 Go 程序進行性能分析,Go 運行時是如何工作的等等。在這篇文章中,我們將着重探討 Go 的垃圾回收器是如何工作的。

在瀏覽互聯網時,我發現了很多關於 Go 語言垃圾回收器的讚譽,而我對垃圾回收器的概念和工作原理只有一個抽象的理解,於是我開始閱讀和學習,並在這裏 [3] 記錄了一些關於垃圾回收的筆記。

這篇博客僅僅是我在閱讀一些關於 Go 的垃圾回收器及其演變歷程的博客後整理出的一些想法和結論的隨筆。

所以,讓我們開始吧。

一點前置知識

Go 是一種以 C 類系統語言爲傳統的值傳遞語言,而不是以大多數託管運行時語言爲傳統的引用導向語言。值導向還有助於外部函數接口。這可能是 Go 與其他 GC 語言不同的最重要因素。

Go 是一種內存管理語言,這意味着大多數時候你不必擔心手動內存管理,因爲運行時會爲你完成大量工作。然而,動態內存分配並不免費,程序的分配模式可以顯著影響其性能:

Go 二進制文件包含整個運行時且沒有即時編譯 [4]。

因爲這個原因,最基本的 Go 二進制文件通常很大 [5]。

垃圾回收簡史

最初的垃圾回收算法是爲單處理器機器和堆很小的程序設計的,由於 CPU 和 RAM 是昂貴的,所以用戶對可見的 GC 暫停沒有問題。當 GC 進來時,你的程序會停止,直到完成堆的全面標記 / 清除。這種類型的算法在不回收時不會降低你的程序速度,也不會增加內存開銷。

簡單的 STW 標記 / 清除的問題在隨着你增加核心和擴大你的堆或分配率時會變得非常糟糕。

Go 的併發收集器

Go 現在的 GC 不是 “分代” 回收 (一種垃圾回收算法)的。它只在後臺運行一個普通的標記 / 清除。這有一些缺點:

要防止這種情況,您需要確保有大量空間,從而增加堆的開銷。

收集器行爲

Go 的垃圾回收是在程序運行時併發進行的。

等候多時了,讓我們看看收集器是如何工作的。

在收集開始時,收集器將經過三個工作階段。其中兩個階段會導致 Stop The World(STW)延遲,另一個階段會導致應用程序吞吐量減慢的延遲。

標記設置 (STW)

當垃圾回收開始時,第一個必須執行的活動是打開寫保護(Write Barrier)。這允許在垃圾回收期間在堆上保持數據完整性,因爲回收器和應用程序的 goroutine 將同時運行。要打開寫保護,必須停止運行的每個應用程序 goroutine。此活動通常非常快,平均每 10 到 30 微秒。這當然前提是應用程序 goroutine 表現正常。

假設在 GC 即將觸發之前有 4 個 goroutine 正在運行。回收器必須停止每個 goroutine 以進行工作。唯一的方法是回收器監視並等待每個 goroutine 調用函數。函數調用保證 goroutine 處於安全點以被停止。如果其中一個 goroutine 沒有調用函數(比如正在執行緊密循環操作 [7]),那麼會發生什麼?

例如,第 4 個 goroutine 正在執行以下代碼:

func stubbornGoroutine(numbers []int32) int {
    var r int32
    for _, v := range numbers {
        // some operation to r
    }

    return r
}

這種情況可能會導致垃圾回收無法開始。因爲當收集器等待時,其他處理器不能服務任何其他協程。因此,協程必須在合理的時間內進行函數調用。

如果一個 goroutine 沒有調用函數,它不會被搶佔,並且在任務結束之前它的 P 不會釋放。這將迫使 “Stop the World” 等待它。

標記階段 (併發)

在開啓寫保護器後,回收器開始標記階段。

首先,回收器爲其自身保留了 25% 可用 CPU 容量 。回收器使用 Goroutine 執行回收工作,並需要應用程序 Goroutine 使用的相同的 P 和 M。

標記階段包括標記堆內存中仍在使用的值。該工作首先通過檢查所有現有 Goroutine 的堆棧以找到指向堆內存的根指針。然後,回收器必須從這些根指針遍歷堆內存圖。

Mark assist

如果收集器確定它需要減緩分配,它將會招募應用程序的 Goroutine 協助 Marking 工作,這稱爲 Mark Assist。任何應用程序 Goroutine 在 Mark Assist 中的時間量與它對堆內存的數據添加量成比例。

Mark Assist 可以幫助更快地完成收集。

收集器的一個目標是消除對 Mark Assist 的需求。如果任意一次收集最終需要大量的 Mark Assist,收集器可以更早開始下一次垃圾收集,以減少下一次收集所需的 Mark Assist 數量。

標記終止(STW)

一旦標記工作完成,下一階段是標記終止。這個階段將關閉寫屏障,執行各種清理任務以及計算下一個回收目標的時刻。在標記階段處於緊密循環的協程也可能導致標記終止 STW 延遲延長。

回收完成後,應用程序協程可以再次使用每個 P,應用程序將回到全速。

併發清除

在收集完成後,還有一個活動稱爲清除。清除是指回收未被標記爲正在使用的堆內存中值所關聯的內存。當應用程序 Goroutine 嘗試分配堆內存中的新值時,該活動會發生。清除的延遲時間會被計入堆內存分配的成本中,並與垃圾回收任何相關的延遲無關。

如何讓運行時知道什麼時候開始回收垃圾?

收集器有一個步伐算法,用於確定何時開始收集。節奏的建模類似於一個控制問題,它試圖找到啓動 GC 週期的正確時間,以達到目標堆大小目標。Go 的默認步伐控制器將嘗試在堆大小加倍時觸發 GC 週期。它通過在當前 GC 週期的標記終止階段設置下一個堆觸發大小來實現這一點。因此,在標記所有活動內存後,它可以決定在當前活動集的總堆大小是目前活動集的 2 倍時觸發下一個 GC。2 倍的值來自運行時使用的變量GOGC,用於設置觸發比率。

一種錯誤的觀念是認爲減緩收集器的速度是提高性能的方法。這個想法是,如果你可以延遲下一次收集的開始,那麼你就是在延遲它造成的延遲。對收集器的同情並不是減緩節奏。

Go 1.5 在 2015 年 8 月發佈,帶有新的低暫停併發垃圾收集器,包括實現了節奏算法 [8]。

採集器延遲成本

有兩種類型的延遲會影響運行中的應用程序。

竊取 CPU 能力

這種奪走的 CPU 能力的影響意味着在回收過程中,您的應用程序不能全速運行。應用程序 Goroutines 現在與收集器的 Goroutines 共享 P 或幫助收集(標記輔助)。

STW 的潛在延遲

第二種被施加的延遲是收集過程中發生的 STW 延遲。STW 時間是指應用程序協程未執行任何應用工作的時間。應用程序實際上已經停止。每次收集都會發生兩次 STW。

減少 GC 延遲的方法是識別並刪除應用程序中不必要的分配。這樣可以從多個方面幫助收集器:

有兩個控制垃圾回收的開關

正如 Rick Hudson 在該文 [9] 中談到。

我們也不打算增加 GC API 的範圍。我們已經運行了近十年,而且有兩個開關,感覺已經足夠了。沒有一個應用程序對我們來說足夠重要,以至於我們需要添加一個新的標誌。

GC 百分比

這調整了你想使用的 CPU 的數量以及您想使用的內存的數量。默認值爲 100,這意味着堆的一半被用於存活內存,另一半用於分配。這可以向任一方向修改。

最大堆內存

MaxHeap 允許程序員設置最大堆大小。Go 對內存不足(OOM)的情況非常敏感;對於內存使用量的臨時高峯,應通過增加 CPU 成本來解決,而不是終止。如果 GC 感到內存壓力,它會通知應用程序應該釋放負載。一旦事情恢復正常,GC 就會通知應用程序可以恢復正常負載。MaxHeap 還提供了更多的調度靈活性。運行時不再對可用內存的多少感到擔心,可以將堆的大小調整到 MaxHeap。

Go 語言 GC 有一個很詳細的說明文檔,可以在源代碼中查看 [10]。

這就是 Go 的垃圾回收器的概述。當然,這並不包括所有內容,我可能遺漏了一些要點,但我試圖總結我所理解的一切。下面是我所遇到的一些非常好的參考資料,請一定要看一看!

參考資料

相關鏈接:

[1]https://github.com/rubinius/rubinius-website-archive/blob/cf54187d421275eec7d2db0abd5d4c059755b577/_posts/2013-06-22-concurrent-garbage-collection.markdown

[2]https://blog.golang.org/ismmkeynote

[3]https://agrim123.github.io/posts/garbage-collection.html

[4]https://en.wikipedia.org/wiki/Just-in-time_compilation

[5]https://golang.org/doc/faq#Why_is_my_trivial_program_such_a_large_binary

[6]https://hellokangning.github.io/en/post/what-is-the-concurrent-mode-failure/

[7]https://stackoverflow.com/a/2213001

[8]https://docs.google.com/document/d/1wmjrocXIWTr1JxU-3EQBI6BK6KgtiFArkG47XK73xIQ/edit#heading=h.4801yvqy4taz

[9]https://blog.golang.org/ismmkeynote

[10]https://github.com/golang/go/blob/master/src/runtime/mgc.go#L5-L127

原文地址:

https://agrim123.github.io/posts/go-garbage-collector.html

原文作者:

Agrim Mittal

本文永久鏈接: https://github.com/gocn/translator/blob/master/2023/w06_Go’s_garbage_collector.md

譯者:朱亞光

校對:haoheipi

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0CXU2jxqiDTOFuk_7AxPCA