一文詳解Golang內(nèi)存管理之棧空間管理
0. 簡介
前面我們分別介紹了堆空間管理的內(nèi)存分配器和垃圾收集,這里我們簡單介紹一下Go中??臻g的管理。
1. 系統(tǒng)棧和Go棧
1.1 系統(tǒng)線程棧
如果我們在Linux中執(zhí)行 pthread_create
系統(tǒng)調(diào)用,進(jìn)程會啟動一個(gè)新的線程,這個(gè)棧大小一般為系統(tǒng)的默認(rèn)棧大小,比如在以下系統(tǒng)中,棧大小是8192KB
,也就是8M大小。
$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 128528 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 4194304 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 515129 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
對于棧上的內(nèi)存,程序員無法直接操作,由系統(tǒng)統(tǒng)一管理,一般的函數(shù)參數(shù)、局部變量(C語言)會存儲在棧上。
1.2 Go棧
Go語言在用戶空間實(shí)現(xiàn)了一套runtime
的管理系統(tǒng),其中就包括了對內(nèi)存的管理,Go的內(nèi)存也區(qū)分堆和棧,但是需要注意的是,Go棧內(nèi)存其實(shí)是從系統(tǒng)堆中分配的內(nèi)存,因?yàn)橥瑯舆\(yùn)行在用戶態(tài),Go的運(yùn)行時(shí)也沒有權(quán)限去直接操縱系統(tǒng)棧。
Go語言使用用戶態(tài)協(xié)程goroutine作為執(zhí)行的上下文,其使用的默認(rèn)棧大小比線程棧高的多,其??臻g和棧結(jié)構(gòu)也在早期幾個(gè)版本中發(fā)生過一些變化:
- v1.0 ~ v1.1 — 最小棧內(nèi)存空間為 4KB;
- v1.2 — 將最小棧內(nèi)存提升到了 8KB;
- v1.3 — 使用連續(xù)棧替換之前版本的分段棧;
- v1.4 — 將最小棧內(nèi)存降低到了 2KB;
2. 棧操作
在前面的《Golang調(diào)度器》系列我們也講過,Go語言中的執(zhí)行棧由runtime.stack
,該結(jié)構(gòu)體中只包含兩段字段,分別表示棧的頂部和底部,每個(gè)棧結(jié)構(gòu)體都在[lo, hi)
的范圍內(nèi):
type stack struct { lo uintptr hi uintptr }
棧的結(jié)構(gòu)雖然非常簡單,但是想要理解 Goroutine 棧的實(shí)現(xiàn)原理,還是需要我們從編譯期間和運(yùn)行時(shí)兩個(gè)階段入手:
- 編譯器會在編譯階段會通過
cmd/internal/obj/x86.stacksplit
在調(diào)用函數(shù)前插入runtime.morestack
或者runtime.morestack_noctxt
函數(shù); - 運(yùn)行時(shí)創(chuàng)建新的 Goroutine 時(shí)會在
runtime.malg
中調(diào)用runtime.stackalloc
申請新的棧內(nèi)存,并在編譯器插入的runtime.morestack
中檢查棧空間是否充足;
當(dāng)然,可以在函數(shù)頭加上//go:nosplit
跳過棧溢出檢查。
2.1 棧初始化
??臻g運(yùn)行時(shí)中包含兩個(gè)重要的全局變量,分別是stackpool
和stackLarge
,這兩個(gè)變量分別表示全局的棧緩存和大棧緩存,前者可以分配小于 32KB 的內(nèi)存,后者用來分配大于 32KB 的??臻g:
var stackpool [_NumStackOrders]struct { item stackpoolItem _ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte } //go:notinheap type stackpoolItem struct { mu mutex span mSpanList } // Global pool of large stack spans. var stackLarge struct { lock mutex free [heapAddrBits - pageShift]mSpanList // free lists by log_2(s.npages) }
其初始化函數(shù)如下,從下也可以看出,Go棧的內(nèi)存都是分配在堆上的:
func stackinit() { if _StackCacheSize&_PageMask != 0 { throw("cache size must be a multiple of page size") } for i := range stackpool { stackpool[i].item.span.init() lockInit(&stackpool[i].item.mu, lockRankStackpool) } for i := range stackLarge.free { stackLarge.free[i].init() lockInit(&stackLarge.lock, lockRankStackLarge) } }
2.2 棧分配
我們在這里會按照棧的大小分兩部分介紹運(yùn)行時(shí)對??臻g的分配。在 Linux 上,_FixedStack = 2048
、_NumStackOrders = 4
、_StackCacheSize = 32768
,也就是如果申請的??臻g小于 32KB,我們會在全局棧緩存池或者線程的棧緩存中初始化內(nèi)存:
//go:systemstack func stackalloc(n uint32) stack { ... thisg := getg() ... var v unsafe.Pointer if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize { order := uint8(0) n2 := n for n2 > _FixedStack { order++ n2 >>= 1 } var x gclinkptr if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" { // thisg.m.p == 0 can happen in the guts of exitsyscall // or procresize. Just get a stack from the global pool. // Also don't touch stackcache during gc // as it's flushed concurrently. lock(&stackpool[order].item.mu) x = stackpoolalloc(order) // 全局棧緩存池 unlock(&stackpool[order].item.mu) } else { c := thisg.m.p.ptr().mcache // 線程緩存的棧緩存中 x = c.stackcache[order].list if x.ptr() == nil { // 不夠就調(diào)用stackcacherefill從堆上獲取 stackcacherefill(c, order) x = c.stackcache[order].list } c.stackcache[order].list = x.ptr().next c.stackcache[order].size -= uintptr(n) } v = unsafe.Pointer(x) } else { ... } ... return stack{uintptr(v), uintptr(v) + uintptr(n)} }
如果申請的內(nèi)存空間過大,運(yùn)行時(shí)會查看runtime.stackLarge
中是否有剩余的空間,如果不存在剩余空間,它也會從堆上申請新的內(nèi)存:
//go:systemstack func stackalloc(n uint32) stack { ... thisg := getg() ... var v unsafe.Pointer if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize { ... } else { var s *mspan npage := uintptr(n) >> _PageShift log2npage := stacklog2(npage) // Try to get a stack from the large stack cache. lock(&stackLarge.lock) if !stackLarge.free[log2npage].isEmpty() { // 從stackLarge拿 s = stackLarge.free[log2npage].first stackLarge.free[log2npage].remove(s) } unlock(&stackLarge.lock) lockWithRankMayAcquire(&mheap_.lock, lockRankMheap) if s == nil { // 從堆拿 // Allocate a new stack from the heap. s = mheap_.allocManual(npage, spanAllocStack) if s == nil { throw("out of memory") } osStackAlloc(s) s.elemsize = uintptr(n) } v = unsafe.Pointer(s.base()) } ... return stack{uintptr(v), uintptr(v) + uintptr(n)} }
2.3 棧擴(kuò)容
在之前我們就提過,編譯器會在cmd/internal/obj/x86.stacksplit
中為函數(shù)調(diào)用插入runtime.morestack
運(yùn)行時(shí)檢查,它會在幾乎所有的函數(shù)調(diào)用之前檢查當(dāng)前 Goroutine 的棧內(nèi)存是否充足,如果當(dāng)前棧需要擴(kuò)容,我們會保存一些棧的相關(guān)信息并調(diào)用runtime.newstack
創(chuàng)建新的棧。
在此期間可能觸發(fā)搶占。
接下來就是分配新的棧內(nèi)存和??截?,這里就不詳細(xì)描述了。
2.4 ??s容
runtime.shrinkstack
??s容時(shí)調(diào)用的函數(shù),該函數(shù)的實(shí)現(xiàn)原理非常簡單,其中大部分都是檢查是否滿足縮容前置條件的代碼,核心邏輯只有以下這幾行:
func shrinkstack(gp *g) { ... oldsize := gp.stack.hi - gp.stack.lo newsize := oldsize / 2 if newsize < _FixedStack { return } avail := gp.stack.hi - gp.stack.lo if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 { return } copystack(gp, newsize) }
如果要觸發(fā)棧的縮容,新棧的大小會是原始棧的一半,不過如果新棧的大小低于程序的最低限制2KB
,那么縮容的過程就會停止。
運(yùn)行時(shí)只會在棧內(nèi)存使用不足1/4
時(shí)進(jìn)行縮容,縮容也會調(diào)用擴(kuò)容時(shí)使用的runtime.copystack
開辟新的??臻g。
以上就是一文詳解Golang內(nèi)存管理之棧空間管理的詳細(xì)內(nèi)容,更多關(guān)于Golang??臻g管理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang之?dāng)?shù)據(jù)校驗(yàn)的實(shí)現(xiàn)代碼示例
這篇文章主要介紹了golang之?dāng)?shù)據(jù)校檢的實(shí)現(xiàn)代碼示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10Go語言定時(shí)器Timer和Ticker的使用與區(qū)別
在Go語言中內(nèi)置的有兩個(gè)定時(shí)器,Timer和Ticker,本文主要介紹了Go語言定時(shí)器Timer和Ticker的使用與區(qū)別,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07