内存逃逸常见问题合集
上文说到到超超完美回答了面试官闭包和defer的组合拳,面试官的下一个问题是关于内存逃逸分析。内存逃逸分析是面试中的常客,工作中往往也会因为内存逃逸导致程序性能莫名的变差,但是我们往往难以察觉到哪里出现了内存逃逸,下面来看看超超在解决这方面问题上有哪些诀窍吧!
一、什么是内存逃逸
面试官:我看你之前学过C++,那刚好你结合C++来给我说一下什么是内存逃逸吧?
考点:内存逃逸概念
超超:程序会被编译器分为栈区、堆区、全局变量区、数据区、代码区共五个区
栈区:主要存储函数的入参、局部变量、出参当中的资源由编译器控制申请和释放
堆区:内存由程序员自己控制申请和释放,往往存放一些占用大块内存空间的变量,或是存在于函数局部但需供全局使用的变量
在C++堆区中的空间是需要程序员自己通过关键字new
和delete
手动释放的,对于初级程序员往往会忘记释放内存空间,导致内存泄漏或者悬空指针等一系列问题。Go的内存分配由编译器决定对象的真正存储位置是在栈上还是在堆上,并管理他的生命周期。内存逃逸就是指原本应该是存储在栈上的变量,因为一些原因被分配在了堆上。
面试官:你刚刚说Go语言中由编译器决定对象真正存储位置,如果使用关键字new
申请的对象还会被存储到栈上吗?
考点:new
的特殊情况
超超:是的,即使是用new
申请的内存,如果编译器发现new
出来的内存在函数结束后就没有使用了且申请内存空间不是很大,那么new
申请的内存空间还是会被分配在栈上,毕竟栈访问速度更快且易与管理。
比如下面这段代码
package main
//函数内部new一块空间,外部无引用
func testNew() {
t := new(int)
*t = 1
}
func main() {
testNew()
}
使用逃逸分析命令go build -gcflags="-m" main.go
./main.go:4:10: new(int) does not escape
./main.go:9:9: new(int) does not escape
可以看到new
申请的内存空间被分配到堆上而不是在栈上。
二、内存逃逸常见情况
面试官:那你能给我列举几种你遇到的内存逃逸的情况吗?
考点:内存逃逸场景
超超:其实工作中遇到过很多次内存逃逸,总结起来有下面四种情况会发生内存逃逸
第一种情况变量在函数外部没有引用,优先放到栈中。最典型的例子就是刚刚说的new
内存分配的问题,当new
出来的内存空间没有被外部引用,且申请的内存不是很大时就会被放在栈上而不是堆上。
第二种情况变量在函数外部存在引用,必定放在堆中
package main
import "fmt"
//返回一个指向局部变量num的指针
func showPoint() *int {
num := 1
point := &num
return point
}
func main() {
var point *int
point = showPoint()
fmt.Println(*point)
}
执行逃逸分析命令go build -gcflags="-m" main.go
./main.go:6:2: moved to heap: num
./main.go:14:14: *point escapes to heap
可以看到局部变量num
从栈逃逸到了堆上。原因也很简单,因为在main函数中对返回的指针point
做了解引用操作,而point
指向的变量num
如果存储在栈上会在函数showpoint结束时被释放,那么在main函数中也就无法对指针point
做解引用的操作了,所以变量num
必须要被放在堆上。
第三种情况超过64k的内存占用放到堆上
package main
func testLarge() {
nums1 := make([]int, 8191)
nums2 := make([]int, 8192)
}
func main() {
testLarge()
}
执行逃逸分析命令go build -gcflags="-m -m" main.go
(参数多一个-m
查看更详细逃逸信息)
./main.go:9:11: inlining call to testLarge func() { nums1 := make([]int, 8191); nums2 := make([]int, 8192) }
./main.go:5:15: make([]int, 8192) escapes to heap:
...
./main.go:9:11: make([]int, 8191) does not escape
当make
申请的内存空间大于8192*sizeof(int)/1024=64个字节时,会到堆上申请内存。因为在Go1.3之后用连续栈取代了分段栈,Go1.4中连续栈的初始大小为2kb,频繁的栈扩缩容会导致性能下降,所以在达到阀值64kb时会在堆上申请内存而不是在栈上。这里还有例子就是在make
申请的切片大小为一个变量时也会在堆上申请内存而不是栈上。
第四种情况make创建的切片值为指针
package main
func testSlice() {
nums := make([]*int, 0)
a := 1
nums[0] = &a
}
func main() {
testSlice()
}
执行逃逸分析命令go build -gcflags="-m" main.go
./main.go:5:2: moved to heap: a
./main.go:4:14: make([]*int, 0) does not escape
这里似乎和前面说的第一种情况变量在函数外部没有引用,优先放到栈中有所违背。假设这里创建的切片存储了大量的指针,那么对于当中的每一个指针都需要做变量在外部的验证,这样大量切片取指针,验证操作都会带来性能的损耗,所以这里索性当切片中存储的是指针时,切片中指针指向的栈上的变量全部放到堆上。
三、栈为什么比堆快
面试官:你知道栈的访问速度为什么比堆快吗?
超超:这里有俩个原因
- 栈有俩个sp和bp俩个寄存器存取值
- 堆需要二次寻值
面试官:刚刚你说了很多make
导致的内存逃逸,那我们来聊聊make
和new
吧!
超超:好呀(:小意思😀
本作品采用《CC 协议》,转载必须注明作者和本文链接
"可以看到 new 申请的内存空间被分配到堆上而不是在栈上。" 说反了,被分配到栈上而不是在堆上