在讨论变量生命周期之前,先来了解下计算机组成里两个非常重要的概念:堆和栈。 什么是栈 栈(Stack)是一种拥有特殊规则的线性表数据结构。 1)概念 栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,LastInFirstOut)的顺序,如下图所示。 图:栈的操作及扩展 往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。 从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的元素数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除了栈顶部的成员)进行任何查看和修改操作。 栈的原理类似于将书籍一本一本地堆起来。书按顺序一本一本从顶部放入,要取书时只能从顶部一本一本取出。 2)变量和栈有什么关系 栈可用于内存分配,栈的分配和回收速度非常快。下面的代码展示了栈在内存分配上的作用: funccalc(a,bint)int{ varcint cab varxint xc10 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)自动决定变量分配方式,提高运行效率 堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在CC语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈,全局变量、结构体成员使用堆分配等。程序员不得不花费很长的时间在不同的项目中学习、记忆这些概念并加以实践和使用。 Go语言将这个过程整合到了编译器中,命名为变量逃逸分析。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。 1)逃逸分析 通过下面的代码来展现Go语言如何使用命令行来分析变量逃逸,代码如下: packagemain importfmt 本函数测试入口参数和返回值情况 funcdummy(bint)int{ 声明一个变量c并赋值 varcint cb 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)的返回值,测试函数返回值没有变量接收时的分析情况。 接着使用如下命令行运行上面的代码: gorungcflagsmlmain。go 使用gorun运行程序时,gcflags参数是编译参数。其中m表示进行内存分配分析,l表示避免程序内联,也就是避免进行程序优化。 运行结果如下: commandlinearguments 。main。go:29:13:aescapestoheap 。main。go:29:22:dummy(0)escapestoheap 。main。go:29:13:main。。。argumentdoesnotescape 00 程序运行结果分析如下: 第2行告知代码的第29行的变量a逃逸到堆。 第3行告知dummy(0)调用逃逸到堆。由于dummy()函数会返回一个整型值,这个值被fmt。Println使用后还是会在main()函数中继续存在。 第4行,这句提示是默认的,可以忽略。 上面例子中变量c是整型,其值通过dummy()的返回值逃出了dummy()函数。变量c的值被复制并作为dummy()函数的返回值返回,即使变量c在dummy()函数中分配的内存被释放,也不会影响main()中使用dummy()返回的值。变量c使用栈分配不会影响结果。 2)取地址发生逃逸 下面的例子使用结构体做数据,来了解结构体在堆上的分配情况,代码如下: packagemain importfmt 声明空结构体测试结构体逃逸情况 typeDatastruct{ } funcdummy()Data{ 实例化c为Data类型 varcData 返回函数局部变量地址 returnc } funcmain(){ fmt。Println(dummy()) } 代码说明如下: 第6行,声明一个空的结构体做结构体逃逸分析。 第9行,将dummy()函数的返回值修改为Data指针类型。 第11行,将变量c声明为Data类型,此时c的结构体为值类型。 第14行,取函数局部变量c的地址并返回。 第18行,打印dummy()函数的返回值。 执行逃逸分析: gorungcflagsmlmain。go commandlinearguments 。main。go:15:9:cescapestoheap 。main。go:12:6:movedtoheap:c 。main。go:20:19:dummy()escapestoheap 。main。go:20:13:main。。。argumentdoesnotescape {} 注意第4行出现了新的提示:将c移到堆中。这句话表示,Go编译器已经确认如果将变量c分配在栈上是无法保证程序最终结果的,如果这样做,dummy()函数的返回值将是一个不可预知的内存地址,这种情况一般是CC语言中容易犯错的地方,引用了一个函数局部变量的地址。 Go语言最终选择将c的Data结构分配在堆上。然后由垃圾回收器去回收c的内存。 3)原则 在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆的问题上,编译器会自动帮助开发者完成这个纠结的选择,但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于Go语言,在Java等语言的编译器优化上也使用了类似的技术。 编译器觉得变量应该分配在堆和栈上的原则是: 变量是否被取地址; 变量是否发生逃逸。