Golang - 内存逃逸详解
一、逃逸分析
内存逃逸分析是编译器在编译优化时,用来决定变量应该分配在堆上还是栈上的工具。
- 了解内存逃逸分析的原理后,就能够理解什么样的变量会被分配在栈上、什么样的变量会被分配在堆上。
- 当你的程序对性能非常敏感,就可以使用内存逃逸工具的分析,查看哪些变量逃逸到了堆上,哪些没有逃逸。然后调整代码,再次提高你的程序性能。
go在编译时会进行内存逃逸分析,同样也给开发人员开放了内存逃逸信息。
在编译时增加-m标志,就会输出内存逃逸信息。
go build -gcflags="-m"
二、内存逃逸的影响
内存逃逸会对程序的性能产生影响。当一个变量逃逸到堆上时,会增加垃圾回收的压力,并且会导致额外的堆分配和内存拷贝操作。相比之下,栈上分配的变量更加高效,因为它们的生命周期受限于函数的执行时间。
逃逸变量和非逃逸变量的区别在于它们的分配位置和生命周期。逃逸变量分配在堆上,生命周期可能会比函数的执行时间长;而非逃逸变量分配在栈上,生命周期与函数的执行时间一致。
三、几种常见的内存逃逸情况
原生类型的逃逸分析
1)变量分配在栈上的情况
当原生类型被取地址且地址被赋值给了一个指针变量,当这个指针变量只是在函数内部使用,则这个原生类型会被分配在栈上(即使是通过new方法分配的)
2)变量逃逸到堆上的情况
如果这个指针变量被以某种形式作为了函数返回值(例如,指针变量是struct中的变量,struct是函数返回值),则这个原生类型被分配在堆上(原因很简单,如果分配在栈上,函数返回后栈中的数据失效,这个指针指向的地址就是无效的)
package main
type (
Foo struct {
A int
B string
}
FooHasPointer struct {
A *int
B string
}
)
// 返回了指向了a的指针,a逃逸到堆上
func escapeValue() *int {
var a int // moved to heap: a
a = 1
return &a
}
// 即使newa是指针类型,但是它只在本函数内起作用(没有被作为返回值,相当于一个局部变量),分配到栈上
func noescapeNew() {
newa := new(int) // noescapeNew new(int) does not escape
*newa = 1
}
// 指向i的指针被存储到foo结构体中返回了,i逃逸到堆上
func escapePointer() FooHasPointer {
var foo FooHasPointer
i := 10 //moved to heap: i
foo.A = &i
foo.B = "a"
return foo
}
// 没有指针,都分配到栈上
func noescapeValue() Foo {
var foo Foo
i := 10
foo.A = i
foo.B = "a"
return foo
}
func main() {
}
执行编译,并且附加-m内存逃逸分析标志和-l(L的小写)禁止内联标志
go build -gcflags "-m -l" main.go
slice类型的逃逸分析
与原生类型的逃逸分析相似,当返回指向slice的指针时,slice逃逸;当返回slice时,只有slice中的数据逃逸
slice的逃逸分为slice本身(即SliceHeader)的逃逸分析,和slice中的元素(即SliceHeader中Data指向的地址)的逃逸分析
1)SliceHeader分配在栈上、Data分配在堆上
当SliceHeader分配在栈上,Data既可以分配在栈上也可以分配在堆上
- 当Data的空间不足、需要动态扩容时,Data会被分配在堆上
- 当初始化slice时,Data所占空间达到64K时,SliceHeader和Data都会被分配在堆上(注意这里的64K边界是在自己的windows和linux机上测试到的,没有找go源码的出处,有可能不准确,理解为Data比较大时会直接分配在堆上比较好。另外除了slice,其他的数据类型如果初始化大小超过某个阈值时,应该也会直接分配在堆上)
2)当SliceHeader分配在堆上,SliceHeader和Data都分配在堆上
package main
import (
"reflect"
"strconv"
"unsafe"
)
// sl为局部变量,SliceHeader没有逃逸,Data由于动态扩容分配在了堆上
func noEscapeSliceWithDataInHeap() {
var sl []byte
println("addr of local(no escape, data in heap) slice = ", &sl)
printSliceHeader(&sl)
for i := 0; i < 10; i++ {
println("append " + strconv.Itoa(i))
sl = append(sl, byte(i))
printSliceHeader(&sl)
}
}
// sl为局部变量,SliceHeader没有逃逸,Data不需要动态扩容,分配在栈上
func noEscapeSliceWithDataInStack() {
sl := make([]byte, 0, 10) // noEscapeSliceWithDataInStack make([]byte, 0, 10) does not escape
println("addr of local(no escape, data in stack) slice = ", &sl)
printSliceHeader(&sl)
for i := 0; i < 10; i++ {
println("append " + strconv.Itoa(i))
sl = append(sl, byte(i))
printSliceHeader(&sl)
}
}
// Data过大,SliceHeader和Data直接分配在堆上
func escapeLargeSlice() {
sl := make([]byte, 0, 1024*64) //make([]byte, 0, 1024 * 64) escapes to heap
println("addr of local(escape, data in heap) slice = ", &sl)
printSliceHeader(&sl)
for i := 0; i < 10; i++ {
println("append " + strconv.Itoa(i))
sl = append(sl, byte(i))
printSliceHeader(&sl)
}
}
// Data没有达到64k,没有逃逸
func noescapeSmallSlice() {
sl := make([]byte, 0, 1024*63+1023) // noescapeSmallSlice make([]byte, 0, 1024 * 63 + 1023) does not escape
println("addr of local(no escape, data in stack) slice = ", &sl)
printSliceHeader(&sl)
for i := 0; i < 10; i++ {
println("append " + strconv.Itoa(i))
sl = append(sl, byte(i))
printSliceHeader(&sl)
}
}
// 返回了sl的指针,SliceHeader和Data都逃逸了
func escapeSliceWithDataInHeap() *[]byte {
sl := make([]byte, 0, 10) //moved to heap: sl // make([]byte, 0, 5) escapes to heap
println("addr of local(slice and data in heap) slice = ", &sl)
printSliceHeader(&sl)
for i := 0; i < 10; i++ {
println("append " + strconv.Itoa(i))
sl = append(sl, byte(i))
printSliceHeader(&sl)
}
return &sl
}
// sl作为返回值,SliceHeader没有逃逸,Data逃逸
func sliceInStackWithDataInHeap() []byte {
sl := make([]byte, 0, 10) //make([]byte, 0, 5) escapes to heap
println("addr of local(slice in stack and data in heap) slice = ", &sl)
printSliceHeader(&sl)
for i := 0; i < 10; i++ {
println("append " + strconv.Itoa(i))
sl = append(sl, byte(i))
printSliceHeader(&sl)
}
return sl
}
func printSliceHeader(p *[]byte) {
ph := (*reflect.SliceHeader)(unsafe.Pointer(p))
println("slice data = ", unsafe.Pointer(ph.Data))
}
func main() {
noEscapeSliceWithDataInHeap()
noEscapeSliceWithDataInStack()
escapeLargeSlice()
noescapeSmallSlice()
escapeSliceWithDataInHeap()
sliceInStackWithDataInHeap()
}
执行编译,并且附加-m内存逃逸分析标志和-l禁止内联标志
go build -gcflags "-m -l" main.go
结果如下:
map类型的逃逸分析
1)不作为函数返回值时,分配在栈上
2)作为函数返回值且返回的不是指针时,map的元素分配在堆上,map本身分配在栈上
3)作为函数返回值且返回的是指针时,map的元素分配在堆上,map本身也分配在堆上
package main
func noEscapeMap() {
sm := make(map[int]int) //noEscapeMap make(map[int]int) does not escape
sm[1] = 1
}
func escapeMap() map[int]int {
sm := make(map[int]int) // make(map[int]int) escapes to heap
sm[1] = 1
return sm
}
func escapeMapPointer() *map[int]int {
sm := make(map[int]int) // moved to heap: sm // make(map[int]int) escapes to heap
sm[1] = 1
return &sm
}
func main() {
noEscapeMap()
escapeMap()
}
执行编译,并且附加-m内存逃逸分析标志和-l禁止内联标志
go build -gcflags "-m -l" main.go
结果如下:
四、如何避免内存逃逸
为了避免内存逃逸,我们可以采取一些实用的技巧和最佳实践:
1)使用局部变量:将变量的作用域限制在函数内部,避免变量逃逸到堆上。
2)使用栈分配:尽量使用栈分配而不是堆分配,因为栈分配的效率更高。
3)避免不必要的堆分配:尽量减少不必要的对象分配,可以通过复用对象或使用对象池来减少堆分配的次数。
五、内存逃逸的优化
为了减少内存逃逸,可以通过优化代码来改善性能:
1)使用指针传递:将大的结构体或数组通过指针传递,避免复制数据。【需要修改原对象值,或占用内存比较大的结构体】
2)使用值传递:对于小的结构体或基本类型,可以使用值传递,避免指针的额外开销。【对于只读的占用内存较小的结构体】
六、内存逃逸的调试和分析工具
为了调试和分析内存逃逸问题,可以使用一些常用的工具和技术:
1)Go编译器的逃逸分析标志:通过设置编译器的逃逸分析标志,可以查看变量是否逃逸。
2)性能工具:可以使用Go语言自带的pprof工具来进行性能分析,通过查看内存分配和堆栈信息,可以帮助我们找出内存逃逸的问题。
3)可以使用Go语言的vet工具来进行静态分析,它可以检查代码中的内存逃逸问题,并给出相应的警告。
4)还可以使用第三方工具如go-torch和go tool trace来进行更详细的性能分析和调试。
七、总结
最后得出go逃逸分析要遵循的两个不变性:
1、指向栈对象的指针不能存在于堆中
2、指向栈对象的指针不能在栈对象回收后存活
注意:内存逃逸go在编译的时候就已经为我们优化好了,学习它是为了将性能提高至极致以及熟悉一些稍底层的知识,普通的代码不需要在逃不逃逸上杠。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」