上一篇,我介绍了一下Go的垃圾回收原理,补一下对象的内存是如何分配的。一个对象是 内存分配->使用->垃圾回收 完成一个流程。
Go语言中有两个地方可以进行分配内存,分别是堆和栈。
堆:一个全局的堆空间,堆区的内存一般由编译器和工程师自己共同进行管理分配,由Runtime Gc来释放。
栈:每个goroutine自身的栈空间,栈一本都由编译器自动进行分配和释放,一般会随着入栈和出栈进行销毁。
区别是:栈分配廉价,而堆分配昂贵。
普通的变量是在堆还是栈上?
这里就引出的另外一个问题:逃逸分析。
逃逸:一句话就是变量的作用域超出来所在的栈区,就是逃逸。
在编译期由静态代码分析来决定一个值是否能被分配在栈帧上,还是需要“逃逸”到堆上。
逃逸分析的好处
func main() {number := getNumber()fmt.Println(number)
}func getNumber() *int {num := rand.Intn(100)return &num
}
从图中可以看出,当main函数想要调用getNumber()函数的时候,必定会从main栈帧到getNumber()栈帧,而如果想通过调用传递参数,比如通过堆空间地址的引用。所以需要将num变量所分配的地址指向堆内存空间中。这就是逃逸分析技术。
也就是:Go 查找所有变量超过当前函数栈侦的,把它们分配到堆上,避免 outlive 变量
分段栈:Go应用程序运行时,每个goroutine都维护着一个自己的栈区,别的goroutine不能使用,初始大小为2KB,运行期间可以动态的增长和收缩。
hot split问题:但是分段栈存在hot split问题,如果一个栈空间满了,会出发扩容,函数返回时会自动销毁,但是如果这个函数在一个循环中,会频繁的分配/销毁,性能会下降。
连续栈:为了解决以上问题,采用连续栈,其实有点像java中的ArrayList扩容机制,默认分配2KB,如果不够的话,那么会创建一个新的空间*2,将数据直接迁移过去,引用地址也进行修改。而且支持动态的缩容。具体的扩容和缩容算法这里不具体细说,感兴趣的可以google。
总结:span = 一个空间规则描述信息(size class) + 至少一个page内存页= 多个object对象
一般小对象通过 mspan 分配内存;大对象则直接由 mheap 分配内存。
三种级别进行分配。
本文从Go内存结构,堆栈两个出发,然后引出逃逸分析,以及其特点。接着分析了分段栈的缺点,引入了连续栈。最后是内存的布局,以及分配原则,主要围绕mcache、mcentral、mheapGo内存管理的三大组件。
资料:Go训练营。
下一篇:mysql8操作语句