在开始剖析Go逃逸分析前,我们要先清楚什么是堆栈。数据结构中有堆栈,内存分配中也有堆栈,两者在定义和用途上虽不同,但也有些许关联,内存分配中栈的压栈和出栈操作,类似于数据结构中的栈的操作方式

内存分配中的堆栈

程序在运行过程中,必不可少的会使用变量、函数和数据,变量和数据在内存中存储的位置可以分为:堆区(Heap)和栈区(Stack),一般由C或C++编译的程序占用内存为:

  • 栈区
  • 堆区
  • 全局区
  • 常量区
  • 程序代码区

软件程序中的数据和变量都会被分配到程序所在的虚拟内存空间中

PUSHRELEASE
malloc

堆空间没有特定的结构,也没有固定的大小,可以动态进行分配和调整,所以内存占用较大的局部变量会放在堆空间上,在编译时不知道该分配多少大小的变量,在运行时也会分配到堆上,在堆上分配内存开销比在栈上大,而且堆上分配的内存需要手动释放,对于 Golang 这种有 GC 机制的语言, 也会增加 GC 压力, 也容易造成内存碎片。

注:栈是线程级的,堆是进程级的

内存逃逸

所谓内存逃逸,就是本该分配于栈空间的变量,被分配到了堆空间,过多的内存逃逸会导致GC压力变大,堆空间碎片化。

Go语言中,变量不能显示的指定分配在栈空间还是堆空间,但是官方回复中大致表示了一个原则:如果局部变量被其他函数捕获,那么就分配在堆上。

逃逸分析

在编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析,通俗来说,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。逃逸分析有两个基本的不变性:

  • 指向栈对象的指针不能存储在堆中
  • 指向栈对象的指针不能超过该栈对象的存活期(即指针不能在栈对象被销毁后依旧存活)

分析工具

go build -gcflags '-m -l' xxx.go
  • -N:禁止编译优化
  • -l:禁止内联
  • -m:逃逸分析
  • -benchmem:压测时打印内存分配统计

通过逃逸分析判断一个变量到底是分配在堆上还是栈上

逃逸场景

指针逃逸

指针逃逸应该是最容易理解的一种情况了,即在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。

createDemo
escapes to heap

动态反射interface{}变量

interface{}interface{}
demofmt.Println()fmt.Println()interface{}
fmt.Printlninterface{}reflect.TypeOf(arg).Kind()interface{}runtime

申请栈空间过大

栈空间大小是有限的,如果编译时发现局部变量申请的空间过大,则会发生内存逃逸,在堆空间上给大变量分配内存

num := make([]int, 0, 8193)int

切片变量自身和元素的逃逸

lencap

2.只指定slice的长度即array,数组本身和元素均在栈上分配,均未发生逃逸

闭包

所谓闭包,就是函数与其所处环境捆绑的组合,也就是说,闭包可以让你在一个内部函数中访问到其外部函数的作用域

Increase()inIncrease()

逃逸分析的作用

  • 通过逃逸分析能确定哪些变量分配到栈空间,哪些分配到堆空间,对空间需要 GC 系统回收资源,GC 系统会有微秒级的 STW,降低 GC 的压力能提高系统的运行效率。
  • 栈空间的分配比堆空间更快性能更好,对于热点数据分配到栈上能提高接口的响应。
  • 栈空间分配的内存,在函数执行完毕后由系统回收资源,不需要 GC 系统参与,也不需要 GC 标记清除,可降低内存的占用
您可能感兴趣的文章: