為什麼你的 64-bit 程式可能佔用巨大的虛擬空間

polarisxu發表於2020-08-11

出於很多目的,我從最新的 Go 系統核心開發原始碼複製了一份程式碼,在一個正常的執行環境中構建(和重新構建)它,在構建版本基礎上週期性地重新構建 Go 程式。近期我在用 ps 檢視我的一個程式的記憶體使用情況時,發現它佔用了約 138 GB 的巨大虛擬空間(Linux ps 命令結果的 VSZ 欄位),儘管它的常駐記憶體還不是很大。某個程式的常駐記憶體很小,但是需要記憶體很大,通常是表示有記憶體洩露,因此我心裡一顫。

(用之前版本的 Go 構建後,根據執行時間長短不同,通常會有 32 到 128 MB 不同大小的虛擬記憶體佔用,比最新版本小很多。)

還好這不是記憶體洩漏。事實上,之後的實驗表明即使是個簡單的 hello world 程式也會有佔用很大的虛擬記憶體。通過檢視程式的 /proc//smaps 檔案((cf)可以發現幾乎所有的虛擬空間是由兩個不可訪問的 map 佔用的,一個佔用了約 8 GB,另一個約 128 GB。這些 map 沒有可訪問許可權(它們取消了讀、寫和可執行許可權),所以它們的全部工作就是專門為地址空間預留的(甚至沒有用任何實際的 RAM)。大量的地址空間。

這就是現在的 Go 在 64 位系統上的低階記憶體管理的工作機制。簡而言之,Go (理論上)從連續的 arena 區域上進行低階記憶體分配,申請 8 KB 的頁;哪些頁可以無限申請儲存在一個巨大的 bitmap。在 64 位機器上,Go 會把全部的記憶體地址空間預留給 bitmap 和 arena 區域本身。程式執行時,當你的 Go 程式真正使用記憶體時,arena bitmap 和記憶體 arena 片段會從簡單的預留地址空間變為由 RAM 備份的記憶體,供其他部分使用。

(bitmap 和 arena 通常是通過給 mmap 傳入 PROT_NONE 引數進行初始化的。當記憶體被使用時,會使用 PROT_READ|PROT_WRITE 重新對映。當釋放時,我不確定它做了什麼,所以對此我不發表意見。)

這個例子是用當前釋出的 Go 1.4 開發版本復現的。之前的版本的 64 位程式執行時會佔用更小的需要空間,雖然讀 Go 1.4 原始碼時我也沒找到原因。

以我的理解,一個有意思的影響是 64 位 Go 程式的大部分記憶體分配都可能佔用至多 128 GB 的空間(也可能在整個執行週期內所有的記憶體分配都會,我不確定)。

瞭解更多細節,請看 src/runtime/malloc2.go 的註釋和 src/runtime/malloc1.gomallocinit()

我不得不說,這個比我最初以為地更有意思也更有教育意義,儘管這意味著檢視 ps 不再是一個檢測你的 Go 程式中記憶體洩露的好方法(溫馨提示,我不確定它曾經是不是)。結論是,檢測這類記憶體使用最好的方法是同時使用 runtime.ReadMemStats()(可以通過 net/http/pprof 暴露出去)和 Linux 的 smem 程式或者養成對有意義的記憶體地址空間佔用生成詳細資訊的習慣。

PS: Unix 通常足夠智慧,可以理解 PROT_NONE 對映不會耗盡記憶體,因此不應該對系統記憶體過量使用的限制進行統計。然而,它們會統計每一個程式的總地址空間進行統計,這意味著你執行 1.4 的 Go 程式時不能真的使用這麼多。由於總記憶體地址空間的最大數幾乎不會達到,因此這似乎不是一個問題。

附錄:在 32 位系統上是怎樣的

所有的資訊都在 mallocinit() 註釋中。簡而言之,就是執行時預留了足夠大的 arena 來處理 2 GB 的記憶體(「僅」佔用 256 MB)但是僅預留 2 GB 中理論上它可以使用的 512 MB 地址空間。如果後續的執行過程中需要更多記憶體,就向作業系統申請另一個塊的地址空間,優先 arena 區域剩下的 1.5 GB 的地址空間中分配。大多數情況下,執行的程式都會正常申請到需要分配的空間。


via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoBigVirtualSize

作者:ChrisSiebenmann 譯者:lxbwolf 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

相關文章