Go学习笔记(day6) | 青训营笔记

86 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第6天

笨人纯小白,笔记包括一些上课学到的知识和课外总结的内容,如有错误请指正!

九、Golang内存管理(一:栈内存)

9.1 栈内存的作用

Golang中栈内存也叫协程栈或者调用栈。协程栈中第一个栈帧是goexit(),goexit()是为了退出后重新调度使用的,同时协程栈中还记录了协程的执行路径,例如下图中,do1()调用了do2()

在函数中声明的局部变量,如果只有函数内部使用的话,那么这个变量会记录在协程栈里面。

Golang中函数之间的参数传递,例如:

    func do1() {
        num := 1    
        num = do2(num)
        fmt.Println(num)
    }

    func do2(num int) int {
        num++
        return num
    }

do2(num int),需要一个参数,而这个参数是通过do1()传给do2()的,那么他们之间的参数传递是通过栈内存来传递的。

不仅函数传参是通过栈内存进行传递,函数的返回值也是通过栈内存传递的。

image.png

所以协程栈的作用:

  1. 记录协程的执行路径
  2. 记录局部变量
  3. 参数传递,返回值传递

9.2 协程栈的位置

C/C++中栈区和堆区是分开的,堆上的内存需要程序员自己去释放,栈上的内存由程序释放。

Golang中栈内存是从堆内存上申请的,初始空间为2KB,所以说Golang协程栈位于Golang堆内存上,而Golang堆内存位于操作系统虚拟内存上。

9.3 协程栈的结构

    package main

    func sum(a, b int) int {
        sum := 0
        sum = a + b
        return sum
    }

    func main() {
        a := 3
        b := 5
        fmt.Println(sum(a, b))
    }

main()调用sum(a,b int)时需要传递参数,在栈帧里面参数的传递顺序是反的,传递参数时在自己的栈帧里开辟空间记录下要传递的参数,因为Golang采用的是值传递,然后会记录sum(a,b int)返回后的指令,也就是上述代码中fmt.PrintLn()image.png

运行sum(a,b int)函数时会首先在函数的栈帧中记录调用者的栈基址,意思就是当函数返回后需要返回到哪一个栈帧。

当代码运行到sum=a+b,sum()函数会到main()函数的栈帧中寻找a、b的值,sum()函数返回时,会将返回值写回它的调用者的栈内存中预留的返回值空间,也就是上图中的sum函数返回值。

9.4 栈扩容策略

1. 分段栈

Golang 1.13之前使用的是分段栈策略,如下图所示:

image.png

分段栈策略

如果栈空间不足,那么会调用newstack创建一个新的栈空间,但是创建新的栈空间和原来的栈空间是不连续的,协程的多个栈空间会以双向链表的形式串联,通过指针找到这些栈空间。

分段栈的优点

没有空间浪费,能够按需为当前协程分配内存,并且及时减少内存的占用。

分段栈的缺点

栈的空间是不连续的,栈指针会在不连续的空间跳转,协程的栈空间处于填满状态时,新的函数调用都会触发栈扩容,会给新的函数开辟一个不连续的栈空间,当这个函数返回后,新开辟的空间使用完毕就会触发栈收缩,如果此时函数调用特别频繁,那么会导致频繁的栈扩容和栈收缩,增加gc压力。

2. 连续栈

Golang 1.13之后使用的栈扩容策略为连续栈,解决了分段栈开辟空间不连续的问题。

连续栈策略

在协程的栈空间不足时,会调用newstack创建一块为原来栈空间大小两倍的栈空间,然后调用copystack将原来的栈空间中所有的内容复制到开辟的栈空间中,将指向旧栈对应变量的指针重新指向新栈(相同变量在栈扩容前后的地址发生变化),最后调用stackfree销毁并回收原来的栈空间,因此连续栈的缺点就是栈扩容时开销大,而优点是栈空间是连续的

连续栈也有栈收缩的情况,当栈空间使用率不足1/4时,会收缩回原来的1/2