对于提高对 stack 的使用效率,避免重复从heap中分配与释放,对其使用了 pool
的概念,runtime
里为共提供了两个pool, 分别为 stackpool
,另一个为 stackLarge
。

stackpool
: 16b~32k 对应通用的大小的stack。获取时通过调用 stackpoolalloc()
, 释放时调用 stackpoolfree()
。
stackLarge
:对应 > 32K 的 stack
在程序全局调度器初始化时会通过调用 stackinit()
实现对 stack
初始化。
当我们执行一个 go func()
语句的时候,runtime
会通过调用 newproc()
函数来创建G。而内部真正创建G的函数为 newproc1()
,在没有G可以复用的情况下,会通过 newg = malg(_StackMin)
语句创建一个包含stack的G。
// Allocate a new g, with a stack big enough for stacksize bytes. func malg(stacksize int32) *g { newg := new(g) if stacksize >= 0 { stacksize = round2(_StackSystem + stacksize) systemstack(func() { newg.stack = stackalloc(uint32(stacksize)) }) newg.stackguard0 = newg.stack.lo + _StackGuard newg.stackguard1 = ^uintptr(0) // Clear the bottom word of the stack. We record g // there on gsignal stack during VDSO on ARM and ARM64. *(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0 } return newg }
对于新创建的g,需要通过调用 stackalloc()
函数为其分配 stacksize
大小stack,那么分配操作它又是如何工作的呢?
stack的申请
根据申请stack的大小分两种情况,一种是 small stack
,另一种是 large stack
,两者采用不同的申请策略。主要涉及了内存申请策略,如果对golang 的内存管理比较了解的话,这块理解起来就显的太过于简单了。建议先阅读一下这篇文章《Golang 内存组件之mspan、mcache、mcentral 和 mheap 数据结构》。
func stackalloc(n uint32) stack { ... 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 { 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)} }
对于 small stack
以可以分两种情况:
- 有
P
:会直接通过从当前 G 绑定的P中的mcache
字段申请,这个字段可以理解为内存资源中心,里面包含有多种不同规格大小的内存块,根据申请大小找到一个可以满足其大小的最小规格的内存区域。 - 无
P
:调用stackpoolalloc()
会直接从全局stack pool
中获取
用户需要重点关注下 stackcacherefill()
函数。
func stackalloc(n uint32) stack { ... 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() { 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)} }
对于 large stack
而言,先从 stackLarge
全局对象 large stack spans 池中查找。
如果未找到的话,则调用 osStackAlloc()
和 mheap_.allocManual()
函数直接从 heap
中直接申请并注册这块内存。
最后根据找到内存的起始地址(低地址)和 申请大小n,返回 stack
结构体。
总结
从上面可以看到对 stack 的存放有两个地方,一个是全局 stack pool
,另一个就是 P
中的 mcache
字段。分配 stack
时需要考虑当前 g
的运行模式,如果运行在 systemstack
则需要直接从 stack pool中分配stack,否则直接从 p.mcache
分配即可。
stack的释放
对于stack的释放,实现函数为 stackfree()
。
在资源申请时根据其申请大小分为两种情况,同样对于stack的释放也是一样。对于small stack
来讲,从哪里申请的用完再放回去即可,操作对象仍是 P下面的 mcache
字段。
而对于 large stack
来讲,则需要放回 stackLarge
全局变量以便复用,避免重复申请产生的开销;但如果当前GC正处于清扫阶段,则直接调用 osStackFree()
和 mheap_.freeManual()
来释放内存。
对于g中stack的释放触发条件有:
- 当一个g运行结束的时候,可能会释放stack(只是有可能)参考:https://blog.haohtml.com/archives/23437;
- GC期间会清理所有包含stack的g,直接释放完所有g的stack, 然后再将这些g放在不包含stack的
sched.gFree.noStack
列表中(这里指的是全局调度器 sched ),参考:https://blog.haohtml.com/archives/27003; - 其它