一些 Go 語言的新學習者老是會對 goroutine 棧內存佔用大小感到很是好奇。這通常是因爲程序員進行無限的函數循環調用致使的。爲了說明這個問題,請思考如下代碼示例(爲使問題更加清晰而使用相對刻意的寫法): 程序員
package main import "fmt" type S struct { a, b int } // String 實現了接口 fmt.Stringer func (s *S) String() string { return fmt.Sprintf("%s", s) // 調用 Sprintf 時會默認調用 s.String() } func main() { s := &S{a: 1, b: 2} fmt.Println(s) }
儘管我不建議你這樣作,但當你嘗試運行這段代碼的時候,你會發現你的機器正在進行大量的運算,甚至變得無響應而使你不得不使用 ctrl + c 來中斷執行,以避免程序最終達到無藥可救的地步;由於我知道你會這樣作,因此我爲你作好了這一步,你能夠直接在 playground 執行這段代碼。 golang
許多程序員都曾經寫過相似的代碼而致使函數的無限循環調用,並使得他們的程序崩潰,但通常狀況下並不足以對他們的機器形成毀滅性破壞。問題是,爲何 Go 的程序就特殊一點的呢? 安全
goroutine 的一個主要特性就是它們的消耗;建立它們的初始內存成本很低廉(與須要 1 至 8MB 內存的傳統 POSIX 線程造成鮮明對比)以及根據須要動態增加和縮減佔用的資源。這使得 goroutine 會從 4096 字節的初始棧內存佔用開始按需增加或縮減內存佔用,而無需擔憂資源的耗盡。 架構
爲了實現這個目標,連接器(5l、6l 和 8l)會在每一個函數前插入一個序文,這個序文會在函數被調用以前檢查判斷當前的資源是否知足調用該函數的需求(備註 1)。若是不知足,則調用 runtime.morestack 來分配新的棧頁面(備註 2),從函數的調用者那裏拷貝函數的參數,而後將控制權返回給調用者。此時,已經能夠安全地調用該函數了。當函數執行完畢,事情並無就此結束,函數的返回參數又被拷貝至調用者的棧結構中,而後釋放無用的棧空間。 函數
經過這個過程,有效地實現了棧內存的無限使用。假設你並非不斷地在兩個棧之間往返,通俗地講叫棧分割,則代價是十分低廉的。 性能
可是我一直注意到一個問題,當你的程序存在函數的無限循環調用而即將致使你的操做系統內存枯竭,而此時又剛好須要分配新的棧頁面,則會從堆中分配內存。 學習
當你的函數無止盡地調用着本身,新的棧頁面會不斷地從堆中分配,繼而使得函數又可以繼續調用本身。我相信這很快就會使程序用光你機器全部空餘的物理內存,交換存儲器也會被大量使用,最終致使你的系統變得很是不穩定。 google
能夠被 Go 使用的堆內存取決於許多方面,包括你的 CPU 架構以及操做系統,但通常依賴於你機器可用的物理內存,所以你的機器會在即將使用完堆內存以前進行大量交換存儲器的操做。 spa
對於 Go 1.1,許多人都但願能夠提高 32 位以及 64 位平臺上堆內存使用的最大限制,這個問題會在某些狀況下變得更加嚴重。好比說,你的機器不太可能擁有 128GB 的物理內存(備註 3)。 操作系統
最後要說的是,這裏有一些 issue 已經涉及到這個問題(issue1、issue2),但仍未找到在不損失性能的狀況下可以處理該問題的一個好的解決方案。
備註:
1. 一樣適用於方法,但方法的接收者本質上就是函數的第一個參數,當討論有關 Go 的分段棧的問題時,沒有必要將它們區別對待。
2. 使用頁面這個詞不表明每次分配的內存額度是固定的 4096 字節,必要時會調用 runtime.morestack 來進行新的分配,但我猜想會與頁面值的倍數相接近。
3. 因爲 Go 1.1 的改動,64 位 Windows 平臺的堆內存被限制在 32GB 以內。
原文地址:http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite