目录

  • 堆和栈
  • 变量和栈的关系
  • 为什么用堆
  • 变量逃逸( Escape Analysis) - 自动决定变量分配方式,提高运行效率
  • 逃逸分析
  • 取地址发生逃逸
  • 原则

堆和栈

栈: LIFO( Last in first out)
堆: 在内存分配中类似于往一个房价摆放各种家具,家具的尺寸有大有小。

变量和栈的关系

func calc(a, b int) int {
	var c int
	c = a * b

	var x int
	x = c * 10

	return x
}

上面的代码在没有任何优化情况下,会进行 c 和 x 变量的分配过程。Go语言默认情况下会将 c 和 x 分配在栈上,这两个变量在 calc() 函数退出时就不再使用,函数结束时,保存 c 和 x 的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。

为什么用堆

分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往空间里摆放家具会存在虽然有足够的空间,但各空间分布在不同的区域,无法有一段连续的空间来摆放家具的问题。此时,内存分配器就需要对这些空间进行调整优化。

堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

变量逃逸( Escape Analysis) - 自动决定变量分配方式,提高运行效率

在c/c++语言中,函数局部变量尽量使用栈;全局变量、结构体成员使用堆分配等。Go将其整合到编译器中,命名为“变量逃逸分析”。这个技术由编译器分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配,即使程序员使用Go语言完成了整个工程后也不会感受到这个过程。

逃逸分析

package main

import (
	"fmt"
)

func dummy(b int) int {
	var c int
	c = b
	return c
}

func void() {

}

func main() {
	var a int
	void()
	fmt.Println(a, dummy(0))
}

命令行执行:

go run -gcflags "-m -l" .\test\test.go

使用go run运行程序时,-gcflags参数是编译参数。其中-m表示进行内存分配分析,-l表示避免程序内联,也就是避免进行程序优化。

输出如下:

# command-line-arguments
test\test.go:20:13: ... argument does not escape
test\test.go:20:13: a escapes to heap
test\test.go:20:22: dummy(0) escapes to heap    
0 0

第2行告知“main的第29行的变量a逃逸到堆”。
第3行告知“dummy(0)调用逃逸到堆”。由于dummy()函数会返回一个整型值,这个值被fmt.Println使用后还是会在其声明后继续在main()函数中存在。
第4行,这句提示是默认的,可以忽略。

上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。c 变量值被复制并作为 dummy() 函数返回值返回,即使 c 变量在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。c 变量使用栈分配不会影响结果。

取地址发生逃逸

package main

import (
	"fmt"
)

// Data 声明空结构图测试结构体逃逸情况
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() 函数的返回值。

运行测试:

go run -gcflags "-m -l" .\test\test.go
# command-line-arguments
test\test.go:13:6: moved to heap: c
test\test.go:20:13: ... argument does not escape
&{}

注意第 3 行出现了新的提示:将c移到堆中。这句话表示,Go 编译器已经确认如果将 c 变量分配在栈上是无法保证程序最终结果的。如果坚持这样做,dummy() 的返回值将是 Data 结构的一个不可预知的内存地址。这种情况一般是 C/C++ 语言中容易犯错的地方:引用了一个函数局部变量的地址。Go 语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。

原则

在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆上的问题。编译器会自动帮助开发者完成这个纠结的选择。但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于Go语言,在Java等语言的编译器优化上也使用了类似的技术。

编译器觉得变量应该分配在堆和栈上的原则是:

  • 变量是否被取地址。
  • 变量是否发生逃逸。