这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战
栈空间一般由编译器自动进行管理,负责维护函数调用所需要的东西(参数、返回地址,返回值,局部变量)
栈和堆都是对不同功能、不同区域的内存的划分
栈空间底层的内存管理实际上在前面的内存分配中已经提到了
设计原理
从底层往上说哈
寄存器
CPU中的重要组件与资源
- 读写速度最快
- 存储能力有限
栈操作至少会使用两个以上的寄存器
- SP:存储栈顶地址
- BP:存储调用的基地址指针
栈空间都是从高地址往低地址扩展的
- 申请栈空间指的是,获取一个新的栈顶地址,然后往下写
-
- 修改值的方式获得栈空间,比起堆空间的申请调度当然是快很多的
- 什么是stack overflow呢?
线程栈
用于存储用户在该线程上进行函数调用时的参数、返回值、返回地址、局部变量等
- 大小取决于架构
-
- 一般来说使用的空间并不多(除非巨深的函数调用
- 更多的用户自己分配的空间、执行上下文是放在G里面的
-
- G自己也会涉及到函数调用,所以G内部是有自己的栈空间的
逃逸分析
众所周知,C C++是可以由程序员自己控制变量的存储位置(堆还是栈),可能会出现以下情况
- 栈上指针指向堆,堆对象回收后,出现悬挂指针
- 堆上指针指向栈,栈对象回收后,出现堆的悬挂指针,有安全问题
int *dangling_pointer() {
int i = 2;
return &i;
}
i作为堆对象,返回值(位于栈上)为指向其的指针,i回收后,该指针指向就非法了,获取到该指针返回值的函数比较危险
go中使用逃逸分析的方式,由编译器决定哪一些对象需要分配到栈上、哪些分配到堆上
通过改变变量分配位置的方式,维护以下两个不变性
- 指向栈对象的指针只能在栈中
- 栈指针指向的对象只能在栈中
- 当栈对象失效(回收后),指向该对象的指针不能存活
在上面那个例子中,i会被分到栈上面
分析过程
- 编译器构建AST(抽象语法树)
- 构建变量之间的带权重的有向图
- 根据不变性进行对象的分配
栈操作
栈结构:
type stack struct {
lo uintptr
hi uintptr
}
函数调用时需要在栈上分配空间,因此编译阶段需要在调用函数前插入 runtime.morestack 或者 runtime.morestack_nocktxt
- 这样的话就可以在进行函数调用之前进行栈空间的检测以及分配
- 在创建新的G之前也会调用runtime.stackalloc进行栈空间的分配
整个栈管理的过程分为四个部分
- 栈初始化
- 栈分配
- 栈扩容
- 栈缩容
栈初始化
主要有两个全局变量
- runtime.stackpool:全局栈缓存 ,分配小于32KB的栈空间
- runtime.stackLarge:大栈缓存,分配大于32KB的栈空间
这两个全局变量都和mspan密不可分,栈、堆可以理解为同一种内存管理单元分配下的不同组合
初始化时会调用runtime.stackinit初始化这些全局变量
使用全局变量进行内存分配的话,不同线程(P)之间分配空间时带来的锁竞争问题会极大降低效率
- 每个线程的mcache中带有栈缓存来减少全局的锁竞争(32KB以下栈空间的分配)
栈分配
在进行G的初始化的时候,会为G分配大小足够的栈空间:
- 栈空间较小,从mcache、全局栈缓存进行分配
-
- mcache不足,会继续走内存申请流程
- 栈空间较大,从全局的stackLarge中获取
- 栈空间较大且stackLarge没有空间了,会从堆上申请一片足够大的内存空间
栈扩容
还记得最开始的时候在每次调用函数之前插入的morestack吗,函数调用前,检测当前G的栈空间是否足够,不足的话需要进行扩容
- 保存一定信息
- 调用newstack创建新的栈
-
- 如果需要的栈空间大于最大栈空间:stack overflow
- 调用stackalloc进行栈空间的分配
- 开始进行栈空间的拷贝
-
- 调整stack栈顶、栈底地址,计算变动信息
- 拷贝空间
- 调整原有所有指向栈变量的指针地址
栈缩容
流程与扩容类似,其缩容条件是:
- 新栈大小为原有的一半
- 新栈空间大于2KB