go协程栈底层讲解

文章目录

    • 1. go的栈在堆上?
      • 1.1 go 协程栈的作用
      • 1.2 go 协程栈的位置
      • 1.3 go 协程栈结构
      • 1.4 参数传递
      • 1.5 小结
      • 1.6 思考
    • 2. 协程栈不够大怎么办呢
      • 2.1 局部变量太大
        • 2.1.1 指针逃逸
        • 2.1.2 空接口逃逸
        • 2.2.3 大变量逃逸
      • 2.2 栈帧太多
        • 2.2.1 分段栈(1.13版本使用)
        • 2.2.2 连续栈
      • 2.3 小结

前面几章内容
(一) go协程栈底层讲解
(二) go的堆内存结构分析
(三) 高级语言垃圾回收思路和如何减少性能影响原理分析

本节主要分为三节讲解栈内存(go中的协程栈、调用栈)、堆内存、go中的垃圾回收底层相关知识
这几节知识和go语言在高并发特性息息相关,所以很值得分析分析,同时也能大佬们在设计程序语言时的思想

1. go的栈在堆上?

1.1 go 协程栈的作用

我们之前学习go的协程栈时,有一个程序内部的示意图,也就是下面这个样子。整体区域就是go中的栈区(RAM stack),里面是放go的栈内存的,中间的小块是放go一个协程的协程栈,我们之前学到,一个协程栈的第一个方法是goexit() ,它是为了退出之后重新进行调度用户方法的,后面的就是用户的一个一个方法了。
go协程栈底层讲解_第1张图片
协程除了上面能记录协程执行的路径,另外还能存储一些信息,比如局部变量、函数传参、函数的返回值。这些都与c/c++ 类似。

小结一下:go 协程栈的作用

  • 协程的执行路径 (do1—>do2)
  • 局部变量
  • 函数传参
  • 函数返回值

1.2 go 协程栈的位置

  • go 的协程栈位于go 的堆内存上
  • go堆内存位于操作系统的虚拟内存上(这里是操作系统为每一个进程分配的虚拟内存,不是物理机中虚拟内存的概念)

1.3 go 协程栈结构

有这么一段代码, 我们来分析一下里面的协程栈结构
go协程栈底层讲解_第2张图片

  1. 主协程栈

go协程栈底层讲解_第3张图片
2. 通过runtime.main 进入用户的main,这里的main.main 需要开多大,是由编译器分析的
go协程栈底层讲解_第4张图片
3. 填入程序的参数,其中函数参数进入栈帧是最后一个参数开始的
go协程栈底层讲解_第5张图片
4. 程序执行的过程步骤

go协程栈底层讲解_第6张图片

1.4 参数传递

从上面的例子分析

  1. Go 使用参数拷贝传递(值传递)
  2. 传递结构体时: 会拷贝结构体中的全部内容(所以在使用结构体时,尽量用指针,虽然也是拷贝传递,但是是拷贝的地址,大大减少了拷贝效率)

1.5 小结

  1. 协程栈记录了协程的执行现场
  2. 协程栈还负责记录局部变量,传递参数和返回值
  3. Go 使用参数拷贝传递

1.6 思考

由于每个协程栈是紧挨着的(下面示意图),那么问题来了

go协程栈底层讲解_第7张图片
协程栈不够大怎么办? 如果当中有一个协程栈不够大,但是下面有其他的协程栈,这时应该怎么办?

通过资料发现,这些情况能导致协程栈大

  1. 本地变量太大
  2. 栈帧太多(递归层数过多)(在c中就是栈溢出错误)

2. 协程栈不够大怎么办呢

紧接着上一节的思考,学习这一章节

在go 中协程栈只有2k-4k, 不能和线程的栈大小比(线程用兆M为单位计算的)

下面通过两个主要原因进行分析学习

2.1 局部变量太大

局部变量太大往往会出现逃逸现象,我们针对逃逸现象进行分析

什么是逃逸分析呢?

简单来说: 变量是由栈上-----> 堆上(在java语言中貌似也有)

不是所有的变量都能放在协程栈上(下面是里两个原因)

  1. 栈帧回收后,需要继续使用的变量
  2. 太大的变量

在逃逸分析,我们需要分析下面三个具体的原因:

  • 指针逃逸
  • 空接口逃逸
  • 大变量逃逸
2.1.1 指针逃逸

指的是: 本应该是局部变量的,但是返回给了返回值,这时就放在了堆上

术语描述: 函数的返回了对象的指针
go协程栈底层讲解_第8张图片

2.1.2 空接口逃逸

下面是一个简单的打印代码:,但我们看下println的函数接口,
go协程栈底层讲解_第9张图片
它里面是一个空接口,所以可能会逃逸
在这里插入图片描述
分析:

  1. 如果函数参数为interface{}
  2. 函数的实参很可能会逃逸
  3. 因为interface{}类型的函数往往会使用反射(因为反射要求的对象在堆上,在栈上很难做)
2.2.3 大变量逃逸
  1. 过大的变量会导致栈空间不足
  2. 在64位机器中,一般超过64KB的变量会逃逸

2.2 栈帧太多

在方法中如果出现栈帧太多的情况,程序内部会采用栈扩容方法进行处理
栈扩容

  1. Go 栈的初始空间为2KB
  2. 在函数调用前判断栈空间(morestack)(协程在调用前这个函数会判断栈空间是否足够)
  3. 必要时对栈进行扩容
  4. 早期使用分段栈,后期使用连续栈(时期指的是go版本 )
2.2.1 分段栈(1.13版本使用)

分段栈的情况是: 假如第一个栈帧空间不够,直接使用图中箭头指向的空间

可以想象一下,当一个栈里面出现大量栈帧空间不够用时,使用这种方法会在不连续的空间来回跳转

优点: 没有空间浪费
缺点: 栈指针会再不连续的空间跳转(当两块空间中有返回值时)
go协程栈底层讲解_第10张图片

2.2.2 连续栈

连续栈: 直接将原先栈空间不够的拷贝到新开辟的栈空间

优点: 空间一直连续
缺点: 伸缩时的开销大

原理: 当空间不足时扩容,变为原来的2倍;当空间使用率不足1/4时缩容,变为原来的1/2。

连续栈的示意图:
go协程栈底层讲解_第11张图片

2.3 小结

  1. 3种特殊情况下,变量可能会分配到堆上
  2. 1.13 之前,Go 使用可伸缩的分段栈
  3. 1.14以后,Go使用连续栈,伸缩时直接使用新栈

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

你可能感兴趣的:(【go专栏】从原理解析go语言,golang,c++,java)