golang 内存逃逸 你应该知道的知识
逃逸分析
原文链接: 一文弄懂 Golang 中的内存逃逸
1.为什么要了解内存逃逸
- 内存逃逸是 Go 语言编程中一个特别需要注意的问题,会影响到程序的性能和稳定性。
了解和掌握 Go 语言中的内存逃逸对于编写高性能和可维护的代码至关重要。通过合理的代码设计和优化技巧可以避免不必要的内存逃逸并提高程序的运行效率。
2.什么是逃逸分析
- 指原本应该分配在栈上分配的内存被分配到了堆中,也就意味着函数返回后,这部分内存不会被自动释放,需要等待垃圾回收处理
- go中内存分配的两种方式
- 栈上分配:函数调用时为局部变量分配内存,函数返回时,内存会自动释放
- 堆上分配:通过 new && make & struct{} 等方式动态分配内存,需要GC来回收内存
3.内存逃逸的影响 - 性能和稳定性
- 1.内存占用增加,因为堆内存不会自动释放,导致程序占用的内存不断增加,如长时间运行的程序会导致系统资源消耗增加至耗尽
- 2.性能下降,相较于站分配,堆分配需要消耗更多的 CPU 和 内存资源,所以导致程序运行速度减慢
- 3.程序不稳定,如程序中有大量内存逃逸,会导致GC频繁工作,从而影响程序的稳定性
4.内存逃逸的原因
- 1.变量的生命周期超出了其作用域,当一个变量在函数外部被引用,比如被赋值给一个包级别的变量或者作为返回值,这个变量就会发生逃逸。
- 2.大对象的分配,对于大型的数据结构,Go 有时会选择在堆上分配内存,即使它们没有在函数外部被引用。
- 3.闭包引用,如果一个函数返回一个闭包,并且该闭包引用了函数的局部变量,那么这些变量也会逃逸到堆上。
- 4.接口动态分配,当一个具体类型的变量被赋值给接口类型时,由于接口的动态特性,具体的值可能会发生逃逸。
- 5.切片和 map 操作,如果对切片进行操作可能导致其重新分配内存,或者向 map 中插入数据,这些操作可能导致逃逸。
5.内存逃逸的检测
- CMD: go build -gcflags '-m' main.go
- 通过 go tool pprof 分析内存使用,结合运行时的 mem cpu 检测分析内存逃逸现象
6.如何避免内存逃逸
- 1.严格限制变量的作用域。如果一个变量只在函数内部使用,就不要将其返回或赋值给外部变量。
- 2.使用值而不是指针,当不必要的时候,尽量使用值传递而不是指针传递。
- 3.池化对象,对于频繁创建和销毁的对象,考虑使用对象池技术进行复用,减少在堆上分配和回收对象的次数。
- 4.尽量避免在循环或频繁调用的函数中创建闭包,以减少外部变量的引用和堆分配,避免使用不必要的闭包,闭包可能会导致内存逃逸。
- 5.优化数据结构,使用固定大小的数据结构,避免使用动态大小的切片和 map。比如使用数组而不是切片,因为数组的大小在编译时就已确定。
- 6.预分配切片和 map 的容量,如果知道切片或 map 的大小,预先分配足够的容量可以避免在运行时重新分配内存。
7.内存逃逸代码示例
package main
import "strconv"
func main() {
// _ = varsOutCall()
//hugeObjectAlloc()
//_ = closedCall()
//interfaceDyCall()
//operateMap()
}
// 1.变量外部引用
func varsOutCall() *int {
a := 123
return &a
}
/**
$ go build -gcflags '-m' main.go
# command-line-arguments
.\main.go:9:6: can inline varsOutCall
.\main.go:3:6: can inline main
.\main.go:4:17: inlining call to varsOutCall
.\main.go:10:2: moved to heap: a
*/
// 2.大对象分配
func hugeObjectAlloc() {
_ = make([]int64, 1000000)
}
/**
$ go build -gcflags '-m' main.go
# command-line-arguments
.\main.go:24:6: can inline hugeObjectAlloc
.\main.go:3:6: can inline main
.\main.go:5:17: inlining call to hugeObjectAlloc
.\main.go:10:6: can inline varsOutCall
.\main.go:5:17: make([]int64, 1000000) escapes to heap
.\main.go:11:2: moved to heap: a
.\main.go:25:10: make([]int64, 1000000) escapes to heap
*/
// 3.闭包引用
func closedCall( ) func() {
a := 123
return func() {
_ = a
}
}
/**
$ go build -gcflags '-m' main.go
# command-line-arguments
.\main.go:41:6: can inline closedCall
.\main.go:43:9: can inline closedCall.func1
.\main.go:3:6: can inline main
.\main.go:6:16: inlining call to closedCall
.\main.go:11:6: can inline varsOutCall
.\main.go:25:6: can inline hugeObjectAlloc
.\main.go:6:16: func literal does not escape
.\main.go:12:2: moved to heap: a
.\main.go:26:10: make([]int64, 1000000) escapes to heap
.\main.go:43:9: func literal escapes to heap
*/
// 4.接口动态分配,可能逃逸
type animal interface {
eat()
}
type dog struct {
name string
}
func (d dog) eat() {
}
func interfaceDyCall() {
var d animal = &dog{"xiaoming"}
d.eat()
}
// 5.切片和map操作,可能逃逸
func operateMap() {
m := make(map[string]int, 1000)
for i := 0; i < 1000000; i++ {
m[strconv.Itoa(i)] = i
}
}