golang runtime 簡析

Go Runtime 的總覽

golang 的 runtime 在 golang 中的地位類似於 Java 的虛擬機,不過 go runtime 不是虛擬機. golang 程序生成可執行文件在指定平臺上即可運行,效率很高, 它和 c/c++ 一樣編譯出來的是二進制可執行文件. 我們知道運行 golang 的程序並不需要主機安裝有類似 Java 虛擬機之類的東西,那是因爲在編譯時,golang 會將 runtime 部分代碼鏈接進去.

golang 的 runtime 核心功能包括以下內容:

  • 協程(goroutine)調度(併發調度模型)
  • 垃圾回收(GC)
  • 內存分配
  • 使得 golang 可以支持如 pprof、trace、race 的檢測
  • 支持 golang 的內置類型 channel、map、slice、string等的實現
  • 等等

下圖 1 是 golang 程序、runtime、可執行文件與操作系統之間的關係. 區別於 Java 需要安裝虛擬機,go 語言的可執行文件已經包含了 golang 的 runtime,它爲用戶的 go 程序提供協程調度、內存分配、垃圾回收等功能.此外還會與系統內核進行交互,從而真正的利用好 CPU 等資源. 本文主要簡單介紹 golang runtime 的併發調度模型、垃圾回收與內存分配.

圖1 go runtime

協程調度模型

調度是操作系統的核心功能了,從計算機誕生以來,任務的調度就一直在不斷改進與發展,以不斷適應計算機的發展. 單任務、多任務、併發、並行等調度. 到如今雲計算時代分佈式調度也十分成熟,由 go 編寫的 kubernetes 已經廣泛用於各個公司的分佈式集羣中.

golang 語言相比其它語言有一個特殊之處,它實現了自己的調度模塊,並不完全是由計算機操作系統進行調度的(進程、線程). golang 原生支持協程 goroutine,區別於線程、進程. goroutine 的調度由 go runtime 進行,這也是 golang 併發效率高的原因之一.

go 在處理協程上,使用了 GPM 調度模型,從而支持高效的併發調度. 如下圖 2 ,內核線程與邏輯處理器是多對多的關係即 M:N. 從而提升併發效率. GPM 各個模塊的解釋如下:

  • G: 即 Goroutine,更輕量級的線程,保存着上下文信息.
  • P: Processor,是邏輯處理器. 將 goroutine 綁定邏輯處理器 P 的本地隊列後,纔會被調度. Processor 提供了相關的執行環境(Context),如內存分配狀態(mcache),任務隊列(G)等
  • M: 它纔是真正的計算資源,是系統線程.
  • 全局隊列(Global Run Queue): 未分配 Processor 的 Goroutine 保存在全局隊列中. Processor 或 M 都可以從全局隊列中取出 G .
  • 本地隊列(Local Run Queue): 是 Processor 的隊列,當隊列爲空時,會從全局隊列或其它隊列補充 Goroutine.
  • sysmon 協程: go runtime 會創建一個 sysmon 協程. 它會定期喚醒檢查 goroutine 和 processor,確保 goroutine 不會長期佔用 CPU 以及 Processor 可以被執行.

GPM 模型

垃圾回收(GC)

垃圾回收機制是編程語言的重要部分,它影響到程序的長久穩定運行. Java、Python 等語言都有自己的垃圾回收機制,而不需要像 c/c++一樣由程序員管理,可以避免大量的內存泄漏.

3.1 常用的垃圾回收算法

  • 引用計數(reference counting): Python 便主要採用的是引用計數的方式,每一個對象都會記錄它的引用數,每當有新的引用則值增加,刪除則減少,直到引用值爲 0 ,則該對象的生命週期結束.
    標記-清掃(mark & sweep): 使用標記清掃算法,未引用的對象並不會立刻被清除,而是被標記. 直到內存耗盡,掛起程序,清掃所有未被引用的對象,然後繼續程序. 標記清掃法跟蹤了 root 訪問的所有對象,它可以有效的處理循環引用. 它有一個問題是需要 STW (stop the world). golang 便是採用的標記-清掃法進行垃圾回收.在 golang 的迭代過程中改進爲三色標記清掃法,用來減少 STW 的影響.
    複製收集(copy and collection): 目前許多商業虛擬機都採用這種垃圾回收算法. 它將內存分爲兩部分,只使用其中一部分,在進行垃圾回收時,將存活的對象複製到另一部分. 然後清理所有第一部分內存使其構成完成一塊,從而避免內存碎片.

3.2 Golang 的三色標記法

golang 的垃圾回收是基於標記清掃算法,這種算法需要進行 STW(stop the world),這個過程就會導致程序是卡頓的,頻繁的 GC 會嚴重影響程序性能. golang 在此基礎上進行了改進,通過三色標記清掃法與寫屏障來減少 STW 的時間.

三色標記

三色標記法的流程如下,它將對象通過白、灰、黑進行標記,參考下圖3的動圖過程:

  1. 所有對象最開始都是白色.
  2. 從 root 開始找到所有可達對象,標記爲灰色,放入待處理隊列。
  3. 遍歷灰色對象隊列,將其引用對象標記爲灰色放入待處理隊列,自身標記爲黑色。
  4. 循環步驟3直到灰色隊列爲空爲止,此時所有引用對象都被標記爲黑色,所有不可達的對象依然爲白色,白色的就是需要進行回收的對象。

三色標記法相對於普通標記清掃,減少了 STW 時間. 這主要得益於標記過程是 「on-the-fly」 的,在標記過程中是不需要 STW 的,它與程序是併發執行的,這就大大縮短了 STW 的時間.

寫屏障

當標記和程序是併發執行的,這就會造成一個問題. 在標記過程中,有新的引用產生,可能會導致誤清掃,即將被清掃的標記爲黑色的對象引用了白色的對象. 這就需要用到屏障技術,golang 採用了寫屏障,作用就是爲了避免這類誤清掃問題. 寫屏障即在內存寫操作前,維護一個約束,從而確保將被清掃的黑色對象不能引用白色對象.

3.3 GC 觸發條件

  1. 當前內存分配達到一定比例則觸發
  2. 2 分鐘沒有觸發過 GC 則觸發
  3. GC 手動觸發,調用 runtime.GC()

圖3 三色標記

內存分配

4.1 Tcmalloc 算法

Tcmalloc(Thread Caching Malloc) 是 google 爲 c 語言開發的運行時內存分配算法. 其核心思想是多級管理,從而降低鎖的粒度. Go runtime 的內存分配就採用了 Tcmalloc 算法.

4.2 golang 內存分配

Go 程序在啓動時,會首先向系統申請一塊內存(虛擬地址空間),然後自己切成小塊進行管理. 將申請的內存,分成 3 個區域,spans、bitmap、arena,如下圖4,這三個區域的作用如下.

  • arena: 就是堆區,go runtime 在動態分配的內存都在這個區域,並且將內存塊分成 8kb 的頁,一些組合起來的稱爲 mspan,成爲 go 中內存管理的基本單元,這種連續的頁一般是操作系統的內存頁幾倍大小.
  • bitmap: 顧名思義,用來標記堆區使用的映射表,它記錄了哪些區域保存了對象,對象是否包含指針,以及 GC 的標記信息.
  • spans: 存放 mspan 的指針,根據 spans 區域的信息可以很容易找到 mspan. 它可以在 GC 時更快速的找到的大塊的內存 mspan.

圖4 golang 內存分配

參考資料

  1. 深入淺出Golang Runtime[https://zhuanlan.zhihu.com/p/95056679]

  2. golang中的runtime包教程[https://studygolang.com/articles/13994?fr=sidebar]

  3. Go垃圾回收機制剖析[http://www.pianshen.com/article/168671039/]

  4. 12 Go 併發調度器模型[https://www.jianshu.com/p/5df0a7e118d8]

  5. 三色標記[https://studygolang.com/articles/12062]

  6. 圖解Go語言內存分配[https://zhuanlan.zhihu.com/p/59125443]

  7. 圖解 TCMalloc[https://zhuanlan.zhihu.com/p/29216091]

  8. Visualizing memory management in Golang[https://deepu.tech/memory-management-in-golang/]

  9. Go: What Does a Goroutine Switch Actually Involve?[https://medium.com/a-journey-with-go/go-what-does-a-goroutine-switch-actually-involve-394c202dddb7]

在這裏插入圖片描述