Go語言的運行環境(runtime)會在goroutine需要的時候動態地分配棧空間,而不是給每個goroutine分配固定大小的內存空間。這樣就避免了需要程序員來決定棧的大小。
分塊式的棧是最初Go語言組織棧的方式。當創建一個goroutine的時候,它會分配一個8KB的內存空間來給goroutine的棧使用。我們可能會考慮當這8KB的棧空間被用完的時候該怎么辦?
為了處理這種情況,每個Go函數的開頭都有一小段檢測代碼。這段代碼會檢查我們是否已經用完了分配的棧空間。如果是的話,它會調用morestack函數。morestack函數分配一塊新的內存作為棧空間,并且在這塊棧空間的底部填入各種信息(包括之前的那塊棧地址)。在分配了這塊新的棧空間之后,它會重試剛才造成棧空間不足的函數。這個過程叫做棧分裂(stack split)。
在新分配的棧底部,還插入了一個叫做lessstack的函數指針。這個函數還沒有被調用。這樣設置是為了從剛才造成棧空間不足的那個函數返回時做準備的。當我們從那個函數返回時,它會跳轉到lessstack。lessstack函數會查看在棧底部存放的數據結構里的信息,然后調整棧指針(stack pointer)。這樣就完成了從新的棧塊到老的棧塊的跳轉。接下來,新分配的這個塊棧空間就可以被釋放掉了。
分塊式的棧讓我們能夠按照需求來擴展和收縮棧的大小。 Go開發者不需要花精力去估計goroutine會用到多大的棧。創建一個新的goroutine的開銷也不大。當 Go開發者不知道棧會擴展到多少大時,它也能很好的處理這種情況。
這一直是之前Go語言管理棧的的方法。但這個方法有一個問題。縮減棧空間是一個開銷相對較大的操作。如果在一個循環里有棧分裂,那么它的開銷就變得不可忽略了。一個函數會擴展,然后分裂棧。當它返回的時候又會釋放之前分配的內存塊。如果這些都發生在一個循環里的話,代價是相當大的。 這就是所謂的熱分裂問題(hot split problem)。它是Go語言開發者選擇新的棧管理方法的主要原因。新的方法叫做棧復制法(stack copying)。
棧復制法一開始和分塊式的棧很像。當goroutine運行并用完棧空間的時候,與之前的方法一樣,棧溢出檢查會被觸發。但是,不像之前的方法那樣分配一個新的內存塊并鏈接到老的棧內存塊,新的方法會分配一個兩倍大的內存塊并把老的內存塊內容復制到新的內存塊里。這樣做意味著當棧縮減回之前大小時,我們不需要做任何事情。棧的縮減沒有任何代價。而且,當棧再次擴展時,運行環境也不需要再做任何事。它可以重用之前分配的空間。
棧的復制聽起來很容易,但實際操作并非那么簡單。存儲在棧上的變量的地址可能已經被使用到。也就是說程序使用到了一些指向棧的指針。當移動棧的時候,所有指向棧里內容的指針都會變得無效。然而,指向棧內容的指針自身也必定是保存在棧上的。這是為了保證內存安全的必要條件。否則一個程序就有可能訪問一段已經無效的棧空間了。
因為垃圾回收的需要,我們必須知道棧的哪些部分是被用作指針了。當我們移動棧的時候,我們可以更新棧里的指針讓它們指向新的地址。所有相關的指針都會被更新。我們使用了垃圾回收的信息來復制棧,但并不是任何使用棧的函數都有這些信息。因為很大一部分運行環境是用C語言寫的,很多被調用的運行環境里的函數并沒有指針的信息,所以也就不能夠被復制了。當遇到這種情況時,我們只能退回到分塊式的棧并支付相應的開銷。
這也是為什么現在運行環境的開發者正在用Go語言重寫運行環境的大部分代碼。無法用Go語言重寫的部分(比如調度器的核心代碼和垃圾回收器)會在特殊的棧上運行。這個特殊棧的大小由運行環境的開發者設置。
這些改變除了使棧復制成為可能,它也允許我們在將來實現并行垃圾回收。
另外一種不同的棧處理方式就是在虛擬內存中分配大內存段。由于物理內存只是在真正使用時才會被分配,因此看起來好似你可以分配一個大內存段并讓操 作系統處理它。下面是這種方法的一些問題
首先,32位系統只能支持4G字節虛擬內存,并且應用只能用到其中的3G空間。由于同時運行百萬goroutines的情況并不少見,因此你很可 能用光虛擬內存,即便我們假設每個goroutine的stack只有8K。
第二,然而我們可以在64位系統中分配大內存,它依賴于過量內存使用。所謂過量使用是指當你分配的內存大小超出物理內存大小時,依賴操作系統保證 在需要時能夠分配出物理內存。然而,允許過量使用可能會導致一些風險。由于一些進程分配了超出機器物理內存大小的內存,如果這些進程使用更多內存 時,操作系統將不得不為它們補充分配內存。這會導致操作系統將一些內存段放入磁盤緩存,這常常會增加不可預測的處理延遲。正是考慮到這個原因,一 些新系統關閉了對過量使用的支持。
- 一、經典(一)
- 二、經典(二)
- 三、經典(三)
- 四、經典(四)
- 五、經典(五)
- 六、經典(六)
- 七、經典(七)
- 八、經典(八)
- 九、經典(九)
- 十、經典(十)
- 十一、經典(十一)
- 十二、經典(十二)
- 其他
- 1、知識點一
- 2、面試集
- 3、負載均衡原理
- 4、LVS相關了解
- 5、微服務架構
- 6、分布式鎖實現原理
- 7、Etcd怎么實現分布式鎖
- 8、Redis的數據結構有哪些,以及實現場景
- 9、Mysql高可用方案有哪些
- 10、Go語言的棧空間管理是怎么樣的
- 11、Goroutine和Channel的作用分別是什么
- 12、Go中的鎖有哪些?三種鎖,讀寫鎖,互斥鎖,還有map的安全的鎖?
- 13、怎么限制Goroutine的數量
- 14、Goroutine和線程的區別?
- 15、中間件原理