摘要:爲了解除這個問題,Go在標記內存的同時跟蹤新的內存分配,並且會去查看垃圾回收器什麼時候需要被觸發。當堆的大小變成原來兩倍的時候,內存分配者會觸發垃圾回收器。

原文: medium.com/a-journey-w…

本文基於Go 1.13

Go的垃圾回收器旨在幫助開發者自動清理應用程序的內存。然而每次跟蹤內存並清理都會影響程序運行的性能。Go的垃圾回收器旨在清理內存的同時也關注性能,主要是以下幾個指標:

  • 當程序暫停的時的兩階段儘可能減少 (這句我也不太知道怎麼翻)
  • 一次垃圾回收的週期少於10ms
  • 一次垃圾回收操作不能佔用超過25%的CPU

這看上去是一個很難實現的目標,本篇文章就是介紹Go是如何完成這些目標的。

堆閾值 Heap Threshold Reached

垃圾回收器關注的第一個指標就是堆的增長。默認情況下,當堆的大小變成原來的兩倍的時候,垃圾回收器會被啓動。這裏有個例子,在循環裏面不斷分配內存

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)
}
複製代碼

追蹤曲線告訴我們,垃圾回收器被觸發

當堆的大小變成原來兩倍的時候,內存分配者會觸發垃圾回收器。這個也可以通過增加參數 GODEBUG=gctrace=1 來將整個生命週期的性能打印出來

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處觸發。

這個閾值通過環境變量GOGC來設置,默認是100%,也就是當堆的大小增加100%時垃圾回收器會被觸發。從性能原因考慮,也爲了避免不斷地開始新的垃圾回收,所以當堆的大小小於4MB*GOGC的時候,儘管GOGC設成100%,但垃圾回收依然不會被觸發

時間閾值 Time Threshold Reached

第二個垃圾回收器關注的之間是兩次垃圾回收時間之間的間隔,如果大於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
複製代碼

協助 Required Assistance

垃圾回收器由兩部分組成

  • 標記內存仍然在使用
  • 將沒有標記正在使用的內存進行替換

在標記階段,Go必須確保標記內存的速度比分配新內存的速度更快。 實際上,如果收集器標記了4Mb的內存,而在同一時間段內程序分配了相同數量的內存,則垃圾收集器必須在完成後立即觸發。

爲了解除這個問題,Go在標記內存的同時跟蹤新的內存分配,並且會去查看垃圾回收器什麼時候需要被觸發。當垃圾回收觸發時第一步開始,他將首先準備給每個processor(GMP中的P)一個goroutine,這個gourtine最開始是處理休眠狀態的,等待標記階段的進行。

跟蹤可以顯示這些goroutines

一旦這些goroutinues產生以後,垃圾回收器會開始進行標記,會去檢查哪個變量是需要被收集以及替換的。標記爲 GC dedicated 的goroutines在沒有搶佔的情況下才會進行標記操作,而標記爲GC空閒的goroutine則在可以直接進行標記操作,因爲它們沒有其他任何需要運行的東西,可以被搶佔。

垃圾回收器現在可以準備將變量標記爲不再使用了。對於每一個變量掃描,都會增加一個counter爲了跟蹤當前工作還有多少剩餘的工作需要被進行。當在垃圾收集期間安排goroutine工作時,Go會將所需的內存分配與已經完成的掃描進行比較,以便比較掃描的速度和分配的要求。如果掃描的速度能比分配的速度快則不需要額外的協助,相反,如果掃描的速度比內存分配的速度要慢,Go會啓動額外的goroutine來協助標記工作。這個圖反應了這個邏輯:

在我們的例子中,goroutine 14 被喚起工作當掃描速度比分配速度低的時候:

CPU限制 CPU limitation

其中一個垃圾回收器的指標是不能佔用超過CPU的25%。這意味着Go在標記階段不能分配多於四分之一的處理器。實際上,這正是我們在前面的示例中看到的,只有兩個goroutines超出了處理器的高度,完全專用於垃圾收集:

我們可以看到,另一個goroutine在他沒有其他工作的時候會爲標記進行工作。然而,當垃圾回收器發出協助請求的時候,Go會在高峯期時超過25%的CPU佔用,如我們所見goroutinue 14

在我們的示例中,在短時間內,將37.5%的處理器(八分之三)分配給標記階段。 但這種情況可能很少見,只有在內存高分配的情況下才會發生。

相關文章