遥不可及|Go 协程的栈内存管理,解密( 二 )
0xc000030738......0xc000081f38前面说了每个goroutine都维护着自己的栈区 , 栈结构是连续栈 , 是一块连续的内存 , 在goroutine的类型定义的源码里我们可以找到标记着栈区边界的stack信息 , stack里记录着栈区边界的高位内存地址和低位内存地址:
typegstruct{stackstack...}typestackstruct{louintptrhiuintptr}栈空间在运行时中包含两个重要的全局变量 , 分别是runtime.stackpool和runtime.stackLarge , 这两个变量分别表示全局的栈缓存和大栈缓存 , 前者可以分配小于32KB的内存 , 后者用来分配大于32KB的栈空间:
//Numberofordersthatgetcaching.Order0isFixedStack//andeachsuccessiveorderistwiceaslarge.//Wewanttocache2KB,4KB,8KB,and16KBstacks.Largerstacks//willbeallocateddirectly.//SinceFixedStackisdifferentondifferentsystems,we//mustvaryNumStackOrderstokeepthesamemaximumcachedsize.//OS|FixedStack|NumStackOrders//-----------------+------------+---------------//linux/darwin/bsd|2KB|4//windows/32|4KB|3//windows/64|8KB|2//plan9|4KB|3_NumStackOrders=4-sys.PtrSize/4*sys.GoosWindows-1*sys.GoosPlan9varstackpool[_NumStackOrders]mSpanListtypestackpoolItemstruct{mumutexspanmSpanList}varstackLargestruct{lockmutexfree[heapAddrBits-pageShift]mSpanList}//go:notinheaptypemSpanListstruct{first*mspan//firstspaninlist,ornilifnonelast*mspan//lastspaninlist,ornilifnone}可以看到这两个用于分配空间的全局变量都与内存管理单元runtime.mspan有关 , 所以我们栈内容的申请也是跟前面文章里的一样 , 先去当前线程的对应尺寸的mcache里去申请 , 不够的时候mache会从全局的mcental里取内存等等 , 想了解这部分具体细节的同学可以参考前面的文章《图解Go内存管理器的内存分配策略》 。
其实从调度器和内存分配的角度来看 , 如果运行时只使用全局变量来分配内存的话 , 势必会造成线程之间的锁竞争进而影响程序的执行效率 , 栈内存由于与线程关系比较密切 , 所以在每一个线程缓存runtime.mcache中都加入了栈缓存减少锁竞争影响 。
typemcachestruct{...alloc[numSpanClasses]*mspanstackcache[_NumStackOrders]stackfreelist...}typestackfreeliststruct{listgclinkptrsizeuintptr}编译器会为函数调用插入运行时检查runtime.morestack , 它会在几乎所有的函数调用之前检查当前goroutine的栈内存是否充足 , 如果当前栈需要扩容 , 会调用runtime.newstack创建新的栈:
funcnewstack(){......//Allocateabiggersegmentandmovethestack.oldsize:=gp.stack.hi-gp.stack.lonewsize:=oldsize*2ifnewsize>maxstacksize{print("runtime:goroutinestackexceeds",maxstacksize,"-bytelimitn")throw("stackoverflow")}//Thegoroutinemustbeexecutinginordertocallnewstack,//soitmustbeGrunning(orGscanrunning).casgstatus(gp,_Grunning,_Gcopystack)//TheconcurrentGCwillnotscanthestackwhilewearedoingthecopysince//thegpisinaGcopystackstatus.copystack(gp,newsize,true)ifstackDebug>=1{print("stackgrowdonen")}casgstatus(gp,_Gcopystack,_Grunning)}旧栈的大小是通过我们上面说的保存在goroutine中的stack信息里记录的栈区内存边界计算出来的 , 然后用旧栈两倍的大小创建新栈 , 创建前会检查是新栈的大小是否超过了单个栈的内存上限 。
oldsize:=gp.stack.hi-gp.stack.lonewsize:=oldsize*2ifnewsize>maxstacksize{print("runtime:goroutinestackexceeds",maxstacksize,"-bytelimitn")throw("stackoverflow")}如果目标栈的大小没有超出程序的限制 , 会将goroutine切换至_Gcopystack状态并调用runtime.copystack开始栈的拷贝 , 在拷贝栈的内存之前 , 运行时会先通过runtime.stackalloc函数分配新的栈空间:
推荐阅读
- 遥不可及|5G却爆出三大致命短板,三大运营商火力全开!半年投资880亿
- 索尼Xperia|索尼新机国内发布:120Hz刷新率+12GB内存,价格真打脸!
- 苹果|iPhone12 运行内存曝光,又被苹果偷偷阉割了
- 飞鸟数码评测|1200元内128GB内存手机选4G的OPPO还是5G的真我
- 未来星空数码科技|128G还是256G?5G时代内存不重要?原来如此
- Nubia|16G运存+256G内存,专业骁龙865旗舰,性价比深入人心!
- iphone12|依旧4GB,iPhone12内存规格出炉,相比上代没提升!
- 小叮当数码|支持6GB内存,多核成绩拉垮不如iPad,iPhone12跑分曝光
- 威锋网|4GB,新 iPhone 运行内存信息曝光:Pro 版本为 6GB,其余则为
- 电脑报|坚果5G新机配置揭晓:1亿像素/超大内存之外还有亮点