一、变量生命周期
1 变量的生命周期
生命周期是指程序执行过程中变量存在的时间段。下面我们分别来看看包变量(全局变量)和局部变量两种变量的生命周期。
① 包变量一直常驻在内存到程序的结束,然后被系统垃圾回收器回收。也就是说包变量的生命周期是整个程序的执行时间。
② 局部变量,例如一个函数中定义的变量。它有一个动态的生命周期:每次执行生命语句时创建一个新的实体,变量一直生存到它变得不可访问(例如没有外部指针指向它,函数退出我们没有路径能访问到这个变量),这时它占用的存储空间就会被回收。
所以我们有结论:
并不是定义在函数内部的局部变量在访问退出函数后就会被回收!
2 堆与栈的分配
学过其他诸如C/C++语言的都知道,变量定义完成一般是分配在堆和栈空间上的。存在哪个空间上是跟你是否动态分配内存有关(new/malloc)。但是在Go语言上这个选择并不是基于使用var和new关键字来声明变量的。
我们看下面两个程序实例:
func calc(a, b int)int {
var c int //默认分配到栈上
c=a*b
var x int
x=c*10 //默认分配到栈上
return x
//函数结束时,保存c和x的栈内存释放
//栈的分配和回收都非常迅速
}
var p *int //全局指针变量
func f(){
var i int
i = 1
p = &x //全局指针变量指向局部变量i
}
func f(){
p := new(int) //局部指针变量,使用new申请的空间
*p = 1
}
上面程序中,第一个程序虽然i是通过var申请的局部变量,但是由于有外部指针指向访问,我们有路径可找到这个空间(变量能够逃逸出函数),所以局部变量i是申请在堆空间上。而第二个程序中p指针变量虽然是使用new申请的空间,但是由于退出函数就没有路径可寻找到它(变量无法逃出函数),所以局部变量p是申请在栈空间上的。
另外我需要提的一点,Go语言区别于C/C++,虽然变量申请在堆空间上,但是它有自动回收垃圾的功能,所以这些堆地址空间也无需我们手动回收,系统会在需要释放的时刻自动进行垃圾回收。
二、变量逃逸
堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈;全局变量、结构体成员使用堆分配等。程序员不得不花费很多年的时间在不同的项目中学习、记忆这些概念并加以实践和使用。
Go 语言将这个过程整合到编译器中,命名为“变量逃逸分析”。这个技术由编译器分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配,即使程序员使用 Go 语言完成了整个工程后也不会感受到这个过程。
逃逸分析
使用下面的代码来展现 Go 语言如何通过命令行分析变量逃逸,代码如下:
package main
import "fmt"
// 本函数测试入口参数和返回值情况
func dummy(b int) int {
// 声明一个c赋值进入参数并返回
var c int
c = b
return c
}
// 空函数, 什么也不做
func void() {
}
func main() {
// 声明a变量并打印
var a int
// 调用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) 的返回值,测试函数返回值没有变量接收时的分析情况。
接着使用如下命令行运行上面的代码:$ go run -gcflags "-m -l" 变量逃逸.go
使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。
程序运行结果分析如下:
输出第 1 行告知“main 的第 20行的变量 a 逃逸到堆”。
第 2 行告知“dummy(0)调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在其声明后继续在 main() 函数中存在。
第 3 行,这句提示是默认的,可以忽略。
上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。c 变量值被复制并作为 dummy() 函数返回值返回,即使 c 变量在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。c 变量使用栈分配不会影响结果。
取地址发生逃逸
下面的例子使用结构体做数据,了解在堆上分配的情况,代码如下:
package main
import "fmt"
// 声明空结构体测试结构体逃逸情况
type Data struct {
}
func dummy() *Data {
// 实例化c为Data类型
var c Data
//返回函数局部变量地址
return &c
}
func main() {
fmt.Println(dummy())
}
代码说明如下:
第 6 行,声明一个空的结构体做结构体逃逸分析。
第 9 行,将 dummy() 函数的返回值修改为 *Data 指针类型。
第 12 行,将 c 变量声明为 Data 类型,此时 c 的结构体为值类型。
第 15 行,取函数局部变量 c 的地址并返回。Go 语言的特性允许这样做。
第 20 行,打印 dummy() 函数的返回值。
执行逃逸分析:
注意第 2行出现了新的提示:将 c 移到堆中。这句话表示,Go 编译器已经确认如果将 c 变量分配在栈上是无法保证程序最终结果的。如果坚持这样做,dummy() 的返回值将是 Data 结构的一个不可预知的内存地址。这种情况一般是 C/C++ 语言中容易犯错的地方:引用了一个函数局部变量的地址。
Go 语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。
原则
在使用 Go 语言进行编程时,Go 语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆上的问题。编译器会自动帮助开发者完成这个纠结的选择。但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于 Go 语言,在 Java 等语言的编译器优化上也使用了类似的技术。
编译器觉得变量应该分配在堆和栈上的原则是:
变量是否被取地址。
变量是否发生逃逸。
特别说明:Go语言中变量逃逸的事情Golang编译器已经帮我们做了,我们只需要了解其过程即可。
➢了解更多Go语言知识:https://study.163.com/course/introduction/1210620804.htm