go语言变量逃逸理解

 

变量内存的分配和变量的生命周期以及堆、栈有着密不可分的关系

什么是栈(stack)

是一种拥有特殊规则的线性表数据结构

栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序,如下图所示。

                                      

往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。


从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的元素数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除了栈顶部的成员)进行任何查看和修改操作。


栈的原理类似于将书籍一本一本地堆起来。书按顺序一本一本从顶部放入,要取书时只能从顶部一本一本取出。

什么是堆(heap)

堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小,分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往这个空间里摆放家具会发现虽然有足够的空间,但各个空间分布在不同的区域,没有一段连续的空间来摆放家具。此时,内存分配器就需要对这些空间进行调整优化,如下图所示。


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

逃逸分析

C/C++中动态分配的内存需要我们手动释放,好处是开发者可以完全掌握控制内存,坏处也是显而易见的:经常出现忘记释放内存,导致内存泄露。所以,很多现代语言都加上了垃圾回收机制。go语言同样也有垃圾回收机制,让开发者有更多的时间专注于业务代码的实现,同时通过逃逸分析来帮助变量内存分配到堆或者是栈,即使是用new()申请到的内存,当分析出变量在函数执行完成之后没有使用,就会把变量分配到栈上(栈分配内存比堆快得多),反之,当该变量在函数执行结束后还有其他的地方对其存在引用,就会被分配在堆上。由于堆不会像栈一样可以自动清理,这将导致go的垃圾回收频繁的处理堆上分配的内存来做回收垃圾处理,这将大幅度的占用系统的开销,以上就是逃逸分析存在的意义。

逃逸分析原理

Go逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸

简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上

这一操作的原则是编译器的分析代码的特征和代码的生命周期的结果:

  1. 如果函数外部没有引用,则优先放到栈中
  2. 如果函数外部存在引用,则必定放到堆中

针对第一条,可能放到堆上的情形:定义了一个很大的数组,需要申请的内存过大,超过了栈的存储能力。

逃逸分析示例

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

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))
}

//go run -gcflags "-m -l" EscapeAnalysis.go
//使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。
//运行结果:
//	# command-line-arguments
//	# command-line-arguments
//	.\EscapeAnalysis.go:25:13: main ... argument does not escape
//	.\EscapeAnalysis.go:25:13: a escapes to heap
//	.\EscapeAnalysis.go:25:22: dummy(0) escapes to heap
//	0 0

//	第 2 行告知“代码的第 25 行的变量 a 逃逸到堆”。
//	第 3 行告知“dummy(0) 调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在 main() 函数中继续存在。
//	第 4 行,这句提示是默认的,可以忽略。
//上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。变量 c 的值被复制并作为 dummy() 函数的返回值返回,即使变量 c 在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。
//变量 c 使用栈分配不会影响结果。
package main

import "fmt"

// 声明空结构体测试结构体逃逸情况
type Data struct {
}

func dummy2() *Data {
	// 实例化c为Data类型
	var c Data
	//返回函数局部变量地址
	return &c
}

//取地址发生逃逸
func main() {
	fmt.Println(dummy2())
}
//	go run -gcflags "-m -l" EscapeAnalysis2.go
//	# command-line-arguments
//	.\EscapeAnalysis2.go:11:6: moved to heap: c
//	.\EscapeAnalysis2.go:18:13: main ... argument does not escape
//	.\EscapeAnalysis2.go:18:20: dummy2() escapes to heap
//	&{}
//注意第 4 行出现了新的提示:将 c 移到堆中。这句话表示,Go 编译器已经确认如果将变量 c 分配在栈上是无法保证程序最终结果的,如果这样做,dummy2() 函数的返回值将是一个不可预知的内存地址,
//这种情况一般是 C/C++ 语言中容易犯错的地方,引用了一个函数局部变量的地址。
//Go语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。

虽然Go语言能够帮助我们完成对内存的分配和释放,但是为了能够开发出高性能的应用我们任然需要了解变量的声明周期。例如,如果将局部变量赋值给全局变量,将会阻止 GC 对这个局部变量的回收,导致不必要的内存占用,从而影响程序的性能

相关参考:

https://www.cnblogs.com/itbsl/p/10476674.html

http://c.biancheng.net/view/22.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值