一文詳解Golang內(nèi)存管理之棧空間管理
0. 簡介
前面我們分別介紹了堆空間管理的內(nèi)存分配器和垃圾收集,這里我們簡單介紹一下Go中??臻g的管理。
1. 系統(tǒng)棧和Go棧
1.1 系統(tǒng)線程棧
如果我們在Linux中執(zhí)行 pthread_create 系統(tǒng)調(diào)用,進程會啟動一個新的線程,這個棧大小一般為系統(tǒng)的默認棧大小,比如在以下系統(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語言在用戶空間實現(xiàn)了一套runtime的管理系統(tǒng),其中就包括了對內(nèi)存的管理,Go的內(nèi)存也區(qū)分堆和棧,但是需要注意的是,Go棧內(nèi)存其實是從系統(tǒng)堆中分配的內(nèi)存,因為同樣運行在用戶態(tài),Go的運行時也沒有權(quán)限去直接操縱系統(tǒng)棧。
Go語言使用用戶態(tài)協(xié)程goroutine作為執(zhí)行的上下文,其使用的默認棧大小比線程棧高的多,其??臻g和棧結(jié)構(gòu)也在早期幾個版本中發(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)體中只包含兩段字段,分別表示棧的頂部和底部,每個棧結(jié)構(gòu)體都在[lo, hi)的范圍內(nèi):
type stack struct {
lo uintptr
hi uintptr
}棧的結(jié)構(gòu)雖然非常簡單,但是想要理解 Goroutine 棧的實現(xiàn)原理,還是需要我們從編譯期間和運行時兩個階段入手:
- 編譯器會在編譯階段會通過
cmd/internal/obj/x86.stacksplit在調(diào)用函數(shù)前插入runtime.morestack或者runtime.morestack_noctxt函數(shù); - 運行時創(chuàng)建新的 Goroutine 時會在
runtime.malg中調(diào)用runtime.stackalloc申請新的棧內(nèi)存,并在編譯器插入的runtime.morestack中檢查棧空間是否充足;
當然,可以在函數(shù)頭加上//go:nosplit跳過棧溢出檢查。
2.1 棧初始化
??臻g運行時中包含兩個重要的全局變量,分別是stackpool和stackLarge,這兩個變量分別表示全局的棧緩存和大棧緩存,前者可以分配小于 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 棧分配
我們在這里會按照棧的大小分兩部分介紹運行時對??臻g的分配。在 Linux 上,_FixedStack = 2048、_NumStackOrders = 4、_StackCacheSize = 32768,也就是如果申請的棧空間小于 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)存空間過大,運行時會查看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 棧擴容
在之前我們就提過,編譯器會在cmd/internal/obj/x86.stacksplit中為函數(shù)調(diào)用插入runtime.morestack運行時檢查,它會在幾乎所有的函數(shù)調(diào)用之前檢查當前 Goroutine 的棧內(nèi)存是否充足,如果當前棧需要擴容,我們會保存一些棧的相關(guān)信息并調(diào)用runtime.newstack創(chuàng)建新的棧。
在此期間可能觸發(fā)搶占。
接下來就是分配新的棧內(nèi)存和棧拷貝,這里就不詳細描述了。
2.4 ??s容
runtime.shrinkstack??s容時調(diào)用的函數(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,那么縮容的過程就會停止。
運行時只會在棧內(nèi)存使用不足1/4時進行縮容,縮容也會調(diào)用擴容時使用的runtime.copystack開辟新的??臻g。
以上就是一文詳解Golang內(nèi)存管理之??臻g管理的詳細內(nèi)容,更多關(guān)于Golang??臻g管理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang調(diào)用dll的接口三種方式小結(jié)
本文介紹了在Go語言中使用syscall包的不同方法加載DLL文件,包括NewLazyDLL、MustLoadDLL、LoadLibrary以及它們在調(diào)用函數(shù)和處理不同數(shù)據(jù)類型轉(zhuǎn)換中的應(yīng)用,感興趣的可以了解一下2025-07-07
golang搭建靜態(tài)web服務(wù)器的實現(xiàn)方法
這篇文章主要介紹了golang搭建靜態(tài)web服務(wù)器的實現(xiàn)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08

