在讨论变量生命周期之前,先来了解下计算机组成里两个非常重要的概念:堆和栈。
什么是栈
栈(Stack)是一种拥有特殊规则的线性表数据结构。
1)概念
栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,LastInFirstOut)的顺序,如下图所示。
图:栈的操作及扩展
往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。
从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的元素数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除了栈顶部的成员)进行任何查看和修改操作。
栈的原理类似于将书籍一本一本地堆起来。书按顺序一本一本从顶部放入,要取书时只能从顶部一本一本取出。
2)变量和栈有什么关系
栈可用于内存分配,栈的分配和回收速度非常快。下面的代码展示了栈在内存分配上的作用:
funccalc(a,bint)int{varcintc=a*b
varxintx=c*10
returnx}
代码说明如下:
第1行,传入a、b两个整型参数。
第2行,声明整型变量c,运行时,c会分配一段内存用以存储c的数值。
第3行,将a和b相乘后赋值给c。
第5行,声明整型变量x,x也会被分配一段内存。
第6行,让c乘以10后赋值给变量x。
第8行,返回x的值。
上面的代码在没有任何优化的情况下,会进行变量c和x的分配过程。Go语言默认情况下会将c和x分配在栈上,这两个变量在calc()函数退出时就不再使用,函数结束时,保存c和x的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。
什么是堆
堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小,分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往这个空间里摆放家具会发现虽然有足够的空间,但各个空间分布在不同的区域,没有一段连续的空间来摆放家具。此时,内存分配器就需要对这些空间进行调整优化,如下图所示。
图:堆的分配及空间
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
变量逃逸(EscapeAnalysis)——自动决定变量分配方式,提高运行效率
堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在C/C++语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈,全局变量、结构体成员使用堆分配等。程序员不得不花费很长的时间在不同的项目中学习、记忆这些概念并加以实践和使用。
Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。
1)逃逸分析
通过下面的代码来展现Go语言如何使用命令行来分析变量逃逸,代码如下:
packagemain
import"fmt"
//本函数测试入口参数和返回值情况funcdummy(bint)int{
//声明一个变量c并赋值varcintc=b
returnc}
//空函数,什么也不做funcvoid(){}
funcmain(){
//声明a变量并打印varaint
//调用void()函数void()
//打印a变量的值和dummy()函数返回fmt.Println(a,dummy(0))}
代码说明如下:
第6行,dummy()函数拥有一个参数,返回一个整型值,用来测试函数参数和返回值分析情况。
第9行,声明变量c,用于演示函数临时变量通过函数返回值返回后的情况。
第16行,这是一个空函数,测试没有任何参数函数的分析情况。
第23行,在main()中声明变量a,测试main()中变量的分析情况。
第26行,调用void()函数,没有返回值,测试void()调用后的分析情况。
第29行,打印a和dummy(0)的返回值,测试函数返回值没有变量接收时的分析情况。
接着使用如下命令行运行上面的代码:
gorun-gcflags"-m-l"main.go
使用gorun运行程序时,-gcflags参数是编译参数。其中-m表示进行内存分配分析,-l表示避免程序内联,也就是避免进行程序优化。
运行结果如下:
#