马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
一、栈和堆的先容
栈和堆的根本概念:
- 栈(Stack):用于存储局部变量和函数调用信息等生命周期与函数调用周期同等的数据。栈上的内存管理是主动举行的,随着函数的进入和退出,相应的内存会被分配和开释。
- 堆(Heap):用于动态内存分配,恰当那些必要在整个步调实验期间保持存在的数据。堆上的内存分配和开释比栈复杂,由垃圾接纳器(Garbage Collector, GC)负责管理。
栈(Stack)的特点
- 巨细:栈的巨细不是固定的,它可以根据必要动态调解,但默认情况下有一个相对较小的初始值。对于Go步调,默认的栈巨细是相对守旧的,但会根据需求主动增长。详细数值大概随Go版本更新而有所变革,但在较新的版本中,栈开始时非常小(比方几KB),并随着函数调用深度和局部变量的数目增长而主动扩展。
- 单个变量巨细:固然理论上栈可以扩展,但是将大块的数据(如非常大的数组或布局体)直接作为局部变量放在栈上并不是一个好主意。这是由于:
- 栈空间的增长是有资本的,频仍地调解栈巨细会影响性能。
- 如果栈上的数据过大,大概会导致栈溢出(stack overflow)错误。
- 在实践中,如果一个变量太大(通常以为高出几KB就比力大了),编译器大概会决定让这个变量“逃逸”到堆上,纵然你没有显式地哀求如许做。这通过逃逸分析实现。
堆(Heap)的特点
- 巨细:堆的巨细弘大于栈,现实上受限于体系的假造内存巨细。这意味着你可以分配比栈更大的对象到堆上。然而,堆上的内存分配和开释相比栈来说更加昂贵,由于它涉及到垃圾接纳机制,而且必要维护额外的元数据来追踪分配的内存块。
- 单个变量巨细:理论上,只要体系有富足的可用内存,你就可以在堆上分配非常大的对象。然而,在现实应用中,分配特别大的对象大概会引发性能标题或内存碎片化标题,尤其是在频仍创建和烧毁大对象的情况下。
现实场景思量
- 栈与堆的选择:一样平常来说,如果你知道某个变量生命周期较短,而且它的巨细适中(通常是几KB以内),那么将其放在栈上是比力符合的。相反,如果变量较大大概其生命周期较长,则应思量在堆上分配。
- 优化发起:只管克制在栈上分配过大的局部变量,以防止不须要的栈增长或栈溢出。同时,也要留意太过依靠堆分配大概导致的内存碎片和垃圾接纳压力。
总结:在Go语言中,固然不能像在C或C++中那样通过显式的语法(如malloc或new利用符)直接控制变量是分配在栈上照旧堆上,但在某些情况下,编译器的逃逸分析,可以编写代码让编译器将变量分配到堆上。
二、编译器的逃逸分析
为了让编译器可否将变量“逃逸”到堆上,有下面几种方法:
- 返回局部变量的地点:如果你从一个函数返回一个局部变量的地点,那么这个局部变量将会被分配在堆上,由于如果它留在栈上,在函数返回后该地点指向的数据将不再有用。
- 利用new(T)为范例T分配内存时,大概利用复合字面量如&T{}时,这些通常会导致对象在堆上分配。
- 闭包捕捉变量:当闭包引用了其外部作用域中的变量,而且这些变量必要在闭包的作用域之外存在时,它们大概会被分配到堆上
代码实例分析- package main
- import "fmt"
- func createInt() *int {
- x := 42 // 这个变量x会被分配到堆上
- return &x
- }
- func makeCounter() func() int {
- count := 0 // 可能会逃逸到堆上,特别是如果闭包被返回并长期使用
- return func() int {
- count++
- return count
- }
- }
- func main() {
- createInt()
- p := new(int) // 分配在堆上
- q := &struct{ a, b int }{1, 2} // 同样,这也会导致结构体在堆上分配
- fmt.Printf("%p\n", p)
- fmt.Printf("%p\n", q)
- makeCounter()
- }
复制代码 利用 Go 编译器的 -gcflags="-m" 参数来查察逃逸分析的效果 - go build -gcflags="-m" yourfile.go
复制代码 上面代码通过逃逸分析输出如下:- D:\project\go_project\demo\stackHeap>go build -gcflags="-m" main.go
- # command-line-arguments
- ./main.go:5:6: can inline createInt
- ./main.go:10:6: can inline makeCounter
- ./main.go:12:9: can inline makeCounter.func1
- ./main.go:20:11: inlining call to createInt
- ./main.go:24:12: inlining call to fmt.Printf
- ./main.go:25:12: inlining call to fmt.Printf
- ./main.go:27:13: inlining call to makeCounter
- ./main.go:6:2: moved to heap: x
- ./main.go:11:2: moved to heap: count
- ./main.go:12:9: func literal escapes to heap
- ./main.go:22:10: new(int) escapes to heap
- ./main.go:23:7: &struct { a int; b int }{...} escapes to heap
- ./main.go:24:12: ... argument does not escape
- ./main.go:25:12: ... argument does not escape
- ./main.go:27:13: func literal does not escape
复制代码
根据逃逸分析输出信息,以下是明白提到分配到堆上的变量和情况:
- x 变量:
- 泉源:./main.go:6:2: moved to heap: x
- 这表明在 createInt 函数中的局部变量 x 被移动到了堆上。这通常是由于函数返回了 x 的地点。
- count 变量:
- 泉源:./main.go:11:2: moved to heap: count
- 在 makeCounter 函数中的局部变量 count 也被移动到了堆上。这是由于闭包捕捉了这个变量,而且该闭包大概在其界说的作用域之外被调用。
- 匿名函数(func literal):
- 泉源:./main.go:12:9: func literal escapes to heap
- 这意味着在 makeCounter 中界说的匿名函数(闭包)逃逸到了堆上。由于闭包捕捉了 count 变量,因此闭包自己也必要在堆上分配以确保其生命周期可以超出界说它的函数作用域。
- 通过 new(int) 分配的对象:
- 泉源:./main.go:22:10: new(int) escapes to heap
- 利用 new(int) 创建的整数指针对象分配在堆上。new(T) 总是导致分配在堆上。
- 布局体字面量的地点:
- 泉源:./main.go:23:7: &struct { a int; b int }{...} escapes to heap
- 获取布局体字面量的地点并将其作为指针利用时,该布局体实例被分配在堆上。
总结:
- x 和 count 局部变量由于它们的地点被返回或被捕捉而分别从栈“逃逸”到堆。
- 匿名函数(闭包) 由于捕捉了局部变量而逃逸到堆上。
- 利用 new(int) 创建的对象总是分配在堆上。
- 获取 布局体字面量的地点 导致该布局体实例被分配在堆上。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |