一个对象本应该分配在栈上,结果分配在了堆上,这就是内存逃逸。
内存逃逸的场景
- 局部指针返回
- 栈空间不足
- 动态类型
- 闭包引用
分析内存逃逸的命令:go build -gcflags='-m -l' memory_analysis.go
,'-l'参数可以禁止内联。
若出现 xxx escapes to heap,则xxx变量是发生了内存逃逸,需要尽量避免内存逃逸,因为栈内存的回收效率比堆内存高很多。
场景
局部指针返回
方法内部定义了一个局部指针,并且将这个指针作为返回值返回时,此时就发生了内存逃逸。
逃逸代码片段举例如下:
代码语言:go复制type student struct {
name string
}
func returnPointer() *student {
stu := &student{
name: "Mike",
}
return stu
}
func main() {
returnPointer()
}
returnPointer
函数返回了student
对象的指针,会导致该对象逃逸到堆上。
逃逸分析输出:
代码语言:txt复制./memory_analysis.go:8:12: &student{...} escapes to heap
&student{...} escapes to heap
表示对象已经逃逸到堆上。
栈空间不足
整个系统的栈空间本身就不大,本测试环境的栈空间为8M,Goroutine的占用空间不到10KB,分配给栈的空间更小,一旦某个对象的占用空间过大,此时就发生了内存逃逸。当栈内单个对象大小超过64KB,则会发生内存逃逸,channel空间不足也会发生逃逸。
ulimit -a
输出栈空间stack size
信息(8192K)如下:
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 63068
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 65535
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 63068
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
逃逸代码片段举例如下:
代码语言:go复制func stackSpace() {
space1 := make([]int, 100, 100)
for i := 0; i < len(space1); i {
space1[i] = i
}
space2 := make([]int, 1000, 1000)
for i := 0; i < len(space2); i {
space2[i] = i
}
}
func main() {
stackSpace()
}
stackSpace
函数分别make了大小为100和10000的两个整型数组,后者可能会因为栈空间不足发生逃逸。
逃逸分析输出:
代码语言:txt复制./memory_analysis.go:15:19: make([]int, 100, 100) does not escape
./memory_analysis.go:20:19: make([]int, 10000, 10000) escapes to heap
make([]int, 100, 100) does not escape
表示大小为100的space1
数组没有发生逃逸。
make([]int, 10000, 10000) escapes to heap
表示大小为10000的space2
数组发生了逃逸。
由于切片一般都是使用在函数传递的场景下,而且切片在 append 的时候可能会涉及到重新分配内存,如果切片在编译期间的大小不能够确认或者大小超出栈的限制,多数情况下都会分配到堆上。
动态分配
当动态分配内存大小,也会发生逃逸,如动态分配数组大小。
逃逸代码片段举例如下:
代码语言:go复制func stackSpace() {
length := 10
space3 := make([]int, length)
for i := 0; i < len(space3); i {
space3[i] = i
}
}
逃逸分析输出:
代码语言:shell复制./memory_analysis.go:26:19: make([]int, length) escapes to heap
make([]int, length) escapes to heap
表示已经逃逸到堆上了,不管初始化的数组大小是多大。
动态类型
当被调用函数的入参是interface或者是不定参数,此时就发生了内存逃逸。
逃逸代码片段举例如下:
代码语言:go复制func main() {
val := "Mike"
fmt.Println("Hello world.", val)
}
fmt.Println
函数本身接收的参数是不定参数,Println源码如下,会导导致内存逃逸。
// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
逃逸分析输出:
代码语言:shell复制./memory_analysis.go:69:16: ... argument does not escape
./memory_analysis.go:69:17: "Hello world." escapes to heap
./memory_analysis.go:69:17: val escapes to heap
"Hello world." escapes to heap
和val escapes to heap
表示这两个变量都已经发生了逃逸。
闭包引用
当函数内含有闭包函数时,此时也会发生内存逃逸。
逃逸代码片段举例如下:
代码语言:go复制func closure() func() string {
name := "Mike"
return func() string {
return "Hello World." name
}
}
func main() {
fmt.Println(closure())
}
closure
函数返回了一个闭包函数,此时会发生逃逸。
逃逸分析输出:
代码语言:shell复制./memory_analysis.go:56:12: func literal escapes to heap
./memory_analysis.go:57:31: "Hello World." name escapes to heap
func literal escapes to heap
表示函数已经发生了逃逸。
局部变量name
因为匿名函数返回出去后,编译器认为应该分配在堆上,也发生了逃逸。
取消逃逸分析
编译器默认会进行逃逸分析,会通过规则判定一个变量是分配到堆上还是栈上。一些函数虽然逃逸分析将其存放到堆上。但是对于我们来说需要放在栈上时,我们就可以使用 //go:noescape
指令强制要求编译器将其分配到函数栈上。比如Go官方的memmove
函数使用了避免逃逸分析指令。
//go:noescape
func memmove(to, from unsafe.Pointer, n uintptr)
该指令适用于一个有声明但没有主体的函数,函数的实现可能是其它语言类型,不允许编译器对其做逃逸分析。
类似的函数还有以下这些:
代码语言:go复制//go:noescape
func mapaccess(t *rtype, m unsafe.Pointer, key unsafe.Pointer) (val unsafe.Pointer)
//go:noescape
func mapaccess_faststr(t *rtype, m unsafe.Pointer, key string) (val unsafe.Pointer)
//go:noescape
func mapassign(t *rtype, m unsafe.Pointer, key, val unsafe.Pointer)
//go:noescape
func mapassign_faststr(t *rtype, m unsafe.Pointer, key string, val unsafe.Pointer)
//go:noescape
func mapdelete(t *rtype, m unsafe.Pointer, key unsafe.Pointer)
若直接对普通的Go函数加上此指令,则会出现如下错误
代码语言:txt复制./memory_analysis.go:8:6: can only use //go:noescape with external func implementations