分段栈(Segment Stacks)热分裂(hot split
分段栈(Segment Stack)
stack
热分裂(hot split)
当一个 stack 即将用完的时候,任意一个函数都会导致堆栈的扩容,当函数执行完返回后,又要触发堆栈的收缩。如果这个操作是在一个for语句里执行的话,则过多的 malloc 和 free 重复操作将导致系统资源开销非常的大。
Stack Frame
连续栈(Contiguous stacks)
连续栈使用了另一种管理机制,每当一个stack 空间不够的时候,直接再申请一个2倍大小的空间,然后再将stack数据拷贝过去,同时修改指向原来stack 的指针到新stack,最后再将旧stack删除。
这种机制可以在当stack 空间快用尽的时候,避免在for语句里频繁触发扩容的问题。也正是官方采用这种机制的原因。
栈的初始化在上篇文章《Golang 的底层引导流程/启动顺序》中介绍过,在应用启动时会有一系列的初始化工作,其中就包括对栈的初始化 (源码),调用函数 。
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)
}
}
stackpoolstackLarge_NumStackOrderslarge stack
在应用刚开始时,会分别对这两种全局stack变量进行初始化。
扩容newstack()runtime.morestack()
当G的堆栈空间不够用时,系统会再申请一块两倍大小(源码)的空间,然后将原堆栈数据拷贝过去,再删除原来的堆栈。
步骤
收缩当一个 G 占用的stack非常大,后期却很少使用的stack,这时候则会有大量的stack处于空间状态,我们需要对空间进行收缩,以释放资源。
收缩原则
- 在GC期间,如果一个goroutine未使用stack的大小占用超过X% ,则需要将其复制到一个small stack中。当需要的时候再进行扩容。
- 在GC期间,如果一个goroutine未使用stack的大小占用超过X%,将其stack guard 降低到一个较小的值。如果它在下一次GC时没有受到保护,则将其复制到一个small stack 中。
1/41/2
栈的释放
对stack的申请与释放请参考 《goroutine栈的申请与释放》
其它Stack Frame
- Local variables
- Saved copies of registers modified by subprograms that could need restoration
- Argument parameters
- Return address
FPSPPCSB
FPSPPCSB
所有用户空间的数据都可以通过FP/SP(局部数据、输入参数、返回值)和SB(全局数据)访问。 通常情况下,不会对SB/FP寄存器进行运算操作,通常情况以会以SB/FP/SP作为基准地址,进行偏移解引用 等操作。
伪SP是一个比较特殊的寄存器,因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部,一般用于定位调用其它函数的参数和返回值。
(SP)+8(SP)a(SP)b+8(SP)
// Stack frame layout // // (x86) // +------------------+ // | args from caller | // +------------------+ <- frame->argp // | return address | // +------------------+ // | caller's BP (*) | (*) if framepointer_enabled && varp < sp // +------------------+ <- frame->varp // | locals | // +------------------+ // | args to callee | // +------------------+ <- frame->sp // // (arm) // +------------------+ // | args from caller | // +------------------+ <- frame->argp // | caller's retaddr | // +------------------+ <- frame->varp // | locals | // +------------------+ // | args to callee | // +------------------+ // | return address | // +------------------+ <- frame->sp
x86armx86
调用栈
这里以一个函数调用过程A->B->C为例了来解释调用栈过程
分配从高到低顺序进行。
推荐阅读针对 heap 的GC: Go:内存管理与内存清理,
参考