原文:medium.com/a-journey-w…golang
本文基於Go 1.13app
Go的垃圾回收器旨在幫助開發者自動清理應用程序的內存。然而每次跟蹤內存並清理都會影響程序運行的性能。Go的垃圾回收器旨在清理內存的同時也關注性能,主要是如下幾個指標:性能
這看上去是一個很難實現的目標,本篇文章就是介紹Go是如何完成這些目標的。ui
垃圾回收器關注的第一個指標就是堆的增加。默認狀況下,當堆的大小變成原來的兩倍的時候,垃圾回收器會被啓動。這裏有個例子,在循環裏面不斷分配內存spa
func BenchmarkAllocationEveryMs(b *testing.B) {
// need permanent allocation to clear see when the heap double its size
var s *[]int
tmp := make([]int, 1100000, 1100000)
s = &tmp
var a *[]int
for i := 0; i < b.N; i++ {
tmp := make([]int, 10000, 10000)
a = &tmp
time.Sleep(time.Millisecond)
}
_ = a
runtime.KeepAlive(s)
}
複製代碼
追蹤曲線告訴咱們,垃圾回收器被觸發3d
當堆的大小變成原來兩倍的時候,內存分配者會觸發垃圾回收器。這個也能夠經過增長參數GODEBUG=gctrace=1
來將整個生命週期的性能打印出來code
gc 8 @0.251s 0%: 0.004+0.11+0.003 ms clock, 0.036+0/0.10/0.15+0.028 ms cpu, 16->16->8 MB, 17 MB goal, 8 P
gc 9 @0.389s 0%: 0.005+0.11+0.007 ms clock, 0.041+0/0.090/0.11+0.062 ms cpu, 16->16->8 MB, 17 MB goal, 8 P
gc 10 @0.526s 0%: 0.046+0.24+0.014 ms clock, 0.37+0/0.14/0.23+0.11 ms cpu, 16->16->8 MB, 17 MB goal, 8 P
複製代碼
週期9是咱們以前看到的運行時間爲389ms的週期。有趣的是這部分: 16->16->8 MB
, 展現了在垃圾回收以前有多少內存正被佔用以及垃圾回收以後剩餘的內存量。咱們清楚地看到,當週期8將堆減小到8MB時,週期9已在16MB處觸發。cdn
這個閾值經過環境變量GOGC來設置,默認是100%,也就是當堆的大小增長100%時垃圾回收器會被觸發。從性能緣由考慮,也爲了不不斷地開始新的垃圾回收,因此當堆的大小小於4MB*GOGC的時候,儘管GOGC設成100%,但垃圾回收依然不會被觸發blog
第二個垃圾回收器關注的之間是兩次垃圾回收時間之間的間隔,若是大於2分鐘,就會強制執行垃圾回收。生命週期
這個能根據給定GODEBUG
參數看到,程序在兩分鐘以後執行了強制的垃圾回收
GC forced
gc 15 @121.340s 0%: 0.058+1.2+0.015 ms clock, 0.46+0/2.0/4.1+0.12 ms cpu, 1->1->1 MB, 4 MB goal, 8 P
複製代碼
垃圾回收器由兩部分組成
在標記階段,Go必須確保標記內存的速度比分配新內存的速度更快。 實際上,若是收集器標記了4Mb的內存,而在同一時間段內程序分配了相同數量的內存,則垃圾收集器必須在完成後當即觸發。
爲了解除這個問題,Go在標記內存的同時跟蹤新的內存分配,而且會去查看垃圾回收器何時須要被觸發。當垃圾回收觸發時第一步開始,他將首先準備給每一個processor(GMP中的P)一個goroutine,這個gourtine最開始是處理休眠狀態的,等待標記階段的進行。
跟蹤能夠顯示這些goroutines
一旦這些goroutinues產生之後,垃圾回收器會開始進行標記,會去檢查哪一個變量是須要被收集以及替換的。標記爲GC dedicated
的goroutines在沒有搶佔的狀況下才會進行標記操做,而標記爲GC空閒的goroutine則在能夠直接進行標記操做,由於它們沒有其餘任何須要運行的東西,能夠被搶佔。
垃圾回收器如今能夠準備將變量標記爲再也不使用了。對於每個變量掃描,都會增長一個counter爲了跟蹤當前工做還有多少剩餘的工做須要被進行。當在垃圾收集期間安排goroutine工做時,Go會將所需的內存分配與已經完成的掃描進行比較,以便比較掃描的速度和分配的要求。若是掃描的速度能比分配的速度快則不須要額外的協助,相反,若是掃描的速度比內存分配的速度要慢,Go會啓動額外的goroutine來協助標記工做。這個圖反應了這個邏輯:
在咱們的例子中,goroutine 14 被喚起工做當掃描速度比分配速度低的時候:
其中一個垃圾回收器的指標是不能佔用超過CPU的25%。這意味着Go在標記階段不能分配多於四分之一的處理器。實際上,這正是咱們在前面的示例中看到的,只有兩個goroutines超出了處理器的高度,徹底專用於垃圾收集:
咱們能夠看到,另外一個goroutine在他沒有其餘工做的時候會爲標記進行工做。然而,當垃圾回收器發出協助請求的時候,Go會在高峯期時超過25%的CPU佔用,如咱們所見goroutinue 14
在咱們的示例中,在短期內,將37.5%的處理器(八分之三)分配給標記階段。 但這種狀況可能不多見,只有在內存高分配的狀況下才會發生。