GO-内存逃逸

GO-内存逃逸,第1张

go内存逃逸总结:

1:函数返回指针型数据

2:切片初始化的空间超过限制或者不确定大小

3:使用interface{}

2.1 什么是逃逸分析

Go 语言中,堆内存是通过垃圾回收机制自动管理的,无需开发者指定。那么,Go 编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段。

2.2 指针逃逸

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

// main_pointer.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 指向的内存不能够分配在栈上,随着函数结束而回收,只能分配在堆上。

编译时可以借助选项 -gcflags=-m,查看变量逃逸的情况:

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

new(Demo) escapes to heap 即表示 new(Demo) 逃逸到堆上了。

2.3 interface{} 动态类型逃逸

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

fmt.Println() 比较特殊,下面的例子不一定合适

例如上面例子中的局部变量 demo

func main() {
	demo := createDemo("demo")
	fmt.Println(demo)
}
./main_pointer.go:18:13: demo escapes to heap

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

fmt 包中的 Println 函数的定义如下:

func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

如果我们将上面的例子修改为:

func test(demo *Demo) {
	fmt.Println(demo.name)
}

func main() {
	demo := createDemo("demo")
	test(demo)
}

这种情况下,局部变量 demo 不会发生逃逸,但是 demo.name 仍旧会逃逸。

2.4 栈空间不足

*** 作系统对内核线程使用的栈空间是有大小限制的,64 位系统上通常是 8 MB。可以使用 ulimit -a 命令查看机器上栈允许占用的内存的大小。

$ ulimit -a
-s: stack size (kbytes)             8192
-n: file descriptors                12800
...

因为栈空间通常比较小,因此递归函数实现不当时,容易导致栈溢出。

对于 Go 语言来说,运行时(runtime) 尝试在 goroutine 需要的时候动态地分配栈空间,goroutine 的初始栈大小为 2 KB。当 goroutine 被调度时,会绑定内核线程执行,栈空间大小也不会超过 *** 作系统的限制。

对 Go 编译器而言,超过一定大小的局部变量将逃逸到堆上,不同的 Go 版本的大小限制可能不一样。我们来做一个实验:

func generate8191() {
	nums := make([]int, 8191) // < 64KB
	for i := 0; i < 8191; i++ {
		nums[i] = rand.Int()
	}
}

func generate8192() {
	nums := make([]int, 8192) // = 64KB
	for i := 0; i < 8192; i++ {
		nums[i] = rand.Int()
	}
}

func generate(n int) {
	nums := make([]int, n) // 不确定大小
	for i := 0; i < n; i++ {
		nums[i] = rand.Int()
	}
}

func main() {
        generate8191()
        
        generate8192()
        
        generate(1)
}
generate8191() 创建了大小为 8191 的 int 型切片,恰好小于 64 KB(64位机器上,int 占 8 字节),不包含切片内部字段占用的内存大小。generate8192() 创建了大小为 8192 的 int 型切片,恰好占用 64 KB。generate(n),切片大小不确定,调用时传入。
$ go build -gcflags=-m main_stack.go
# command-line-arguments
./main_stack.go:9:14: generate8191 make([]int, 8191) does not escape
./main_stack.go:16:14: make([]int, 8192) escapes to heap
./main_stack.go:23:14: make([]int, n) escapes to heap

make([]int, 8191) 没有发生逃逸,make([]int, 8192) 和make([]int, n) 逃逸到堆上,也就是说,当切片占用内存超过一定大小,或无法确定当前切片长度时,对象占用内存将在堆上分配。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/996343.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存