文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Golang学习之内存逃逸分析

2023-01-29 12:01

关注

在开始剖析Go逃逸分析前,我们要先清楚什么是堆栈。数据结构中有堆栈,内存分配中也有堆栈,两者在定义和用途上虽不同,但也有些许关联,内存分配中栈的压栈和出栈操作,类似于数据结构中的栈的操作方式

内存分配中的堆栈

程序在运行过程中,必不可少的会使用变量、函数和数据,变量和数据在内存中存储的位置可以分为:堆区(Heap)和栈区(Stack),一般由C或C++编译的程序占用内存为:

软件程序中的数据和变量都会被分配到程序所在的虚拟内存空间中

每个函数都有自己独立的栈空间,函数的调用参数、返回值以及局部变量大都被分配到该函数的栈空间中, 这部分内存由编译器进行管理,编译时确定分配内存的大小。栈空间有特定的结构和寻址方式,所以寻址十分迅速、开销小,只需要2条 CPU 指令,即压栈出栈 PUSH 和 RELEASE,由于函数栈内存的大小在编译时确定, 所以当局部变量数据太大就会发生栈溢出(Stack Overflow)。当函数执行完毕后, 函数的栈空间被回收, 无需手动去释放。

区别于堆空间,通过 malloc 出来的内存,函数执行完毕后需要“手动”释放,“手动”释放在有垃圾回收的语言中,表现为垃圾回收系统,比如 Golang 语言的 GC 系统,GC 系统通过标记等手段,识别出需要回收的空间。

堆空间没有特定的结构,也没有固定的大小,可以动态进行分配和调整,所以内存占用较大的局部变量会放在堆空间上,在编译时不知道该分配多少大小的变量,在运行时也会分配到堆上,在堆上分配内存开销比在栈上大,而且堆上分配的内存需要手动释放,对于 Golang 这种有 GC 机制的语言, 也会增加 GC 压力, 也容易造成内存碎片。

注:栈是线程级的,堆是进程级的

内存逃逸

所谓内存逃逸,就是本该分配于栈空间的变量,被分配到了堆空间,过多的内存逃逸会导致GC压力变大,堆空间碎片化。

Go语言中,变量不能显示的指定分配在栈空间还是堆空间,但是官方回复中大致表示了一个原则:如果局部变量被其他函数捕获,那么就分配在堆上。

逃逸分析

在编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析,通俗来说,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。逃逸分析有两个基本的不变性:

分析工具

通过编译工具查看详细的逃逸分析过程 go build -gcflags '-m -l' xxx.go 编译参数(-gcflags):

通过逃逸分析判断一个变量到底是分配在堆上还是栈上

逃逸场景

指针逃逸

指针逃逸应该是最容易理解的一种情况了,即在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。

// main.go
package main

import "fmt"

type Demo struct {
	name string
}

func createDemo(name string) *Demo {
	d := new(Demo) // 局部变量 d 逃逸到堆
	d.name = name
	return d
}

func main() {
	demo := createDemo("demo")
	fmt.Println(demo)
}

在这个例子中,函数createDemo的局部变量d发生了逃逸,d作为返回值在main函数中继续使用,因此d指向的内存不能分配在栈上,只能分配在堆上,借助分析工具查看逃逸情况

    $ go build -gcflags=-m main.go 
    ./main.go:10:6: can inline createDemo
    ./main.go:17:20: inlining call to createDemo
    ./main.go:18:13: inlining call to fmt.Println
    ./main.go:10:17: leaking param: name
    ./main.go:11:10: new(Demo) escapes to heap
    ./main.go:17:20: new(Demo) escapes to heap   //指针逃逸
    ./main.go:18:13: demo escapes to heap        //interface{}动态类型逃逸
    ./main.go:18:13: main []interface {} literal does not escape
    ./main.go:18:13: io.Writer(os.Stdout) escapes to heap
    <autogenerated>:1: (*File).close .this does not escape

escapes to heap表示逃逸到堆上了

动态反射interface{}变量

在 Go 语言中,接口即 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。仍以上面的例子

func main() {
   demo := createDemo("demo")
   fmt.Println(demo)
}

./main.go:18:13: demo escapes to heap

demo是main函数的一个局部变量,该变量作为实参传递给fmt.Println(),但是因为fmt.Println()的参数类型是interface{},因此也发生了逃逸

解释fmt.Println 之类的底层系统函数,实现逻辑会基于interface{} 做反射,通过 reflect.TypeOf(arg).Kind() 获取接口对象的底层数据类型,创建具体类型对象时,会发生内存逃逸。由于 interface{} 的变量,编译时无法确定变量类型以及申请空间大小,所以不能在栈空间上申请内存,需要在 runtime 时动态申请,理所应当地发生内存逃逸。

申请栈空间过大

栈空间大小是有限的,如果编译时发现局部变量申请的空间过大,则会发生内存逃逸,在堆空间上给大变量分配内存

func main() {
   num := make([]int, 0, 10000)
   _ = num
}

.\main.go:404:13: make([]int, 0, 10000) escapes to heap   //发生逃逸

经过测试,num := make([]int, 0, 8193) 时刚好发生内存逃逸。在 64 位机上 int 类型为 8B,即 8192 * 8B = 64KB

func main() {
   num1 := make([]int, 0, 8192)
   _ = num1
   
   num2 := make([]int, 0, 8193)
   _ = num2
}

.\main.go:404:14: make([]int, 0, 8192) does not escape
.\main.go:407:14: make([]int, 0, 8193) escapes to heap

切片变量自身和元素的逃逸

1.未指定slice的lencap时,slice自身未发生逃逸,slice的元素发生逃逸。因此slice会动态扩容,编译器不知道容量大小,无法提前在栈空间分配内存,扩容后slice的元素可能会被分配到堆空间,所以slice容器自身也不能被分配到栈空间

type person struct {
   Name string
}

func main() {
   var num []*person
   p1 := &person{
      Name: "ss",
   }
   num = append(num, p1)
}

.\main.go:409:8: &person{...} escapes to heap

2.只指定slice的长度即array,数组本身和元素均在栈上分配,均未发生逃逸

闭包

所谓闭包,就是函数与其所处环境捆绑的组合,也就是说,闭包可以让你在一个内部函数中访问到其外部函数的作用域

func Increase() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

func main() {
	in := Increase()
	fmt.Println(in()) // 1
	fmt.Println(in()) // 2
}

Increase() 返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in 被销毁。很显然,变量 n 占用的内存不能随着函数 Increase() 的退出而回收,因此将会逃逸到堆上。

.\main.go:408:2: moved to heap: n
.\main.go:409:9: func literal escapes to heap
.\main.go:417:13: ... argument does not escape
.\main.go:417:16: in() escapes to heap
.\main.go:418:13: ... argument does not escape
.\main.go:418:16: in() escapes to heap

逃逸分析的作用

到此这篇关于Golang学习之内存逃逸分析的文章就介绍到这了,更多相关Golang内存逃逸内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯