1. 程式人生 > 程式設計 >[譯]Go:垃圾回收器是如何監控你的應用的?

[譯]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,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,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%的處理器(八分之三)分配給標記階段。 但這種情況可能很少見,只有在記憶體高分配的情況下才會發生。