golang内存逃逸分析

 更新时间:2025年06月09日 10:10:21   作者:疯狂的程需猿  
本文主要介绍了golang内存逃逸分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、编译器的逃逸分析

go语言编译器会自动决定把一个变量放在堆上还是放在栈上,编译器会做逃逸分析,当发现变量的作用域没有跑出函数范围(悬空指针),就可以在栈上,否则则必须分配在堆上。

这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。

我们看如下代码:

package main

func main() {
	// 打印返回的指针地址
	println(fool())
	// 0x1400008e000
}

//go:noinline    内置的编译指令,可以强制让 Go 编译器不对指定的函数进行内联优化
func fool() *int {
	var (
		a = 1
		b = 2
		c = 3
		d = 4
		e = 5
	)
	println(&a, &b, &c, &d, &e)
	// 0x14000066f38 0x14000066f30 0x1400008e000 0x14000066f28 0x14000066f20
	return &c
}

我们能看到**c**是返回给main 的局部变量,其中它的地址值是 0x1400008e000 很明显与其他的 a b d e 不是连续的。

我们用go tool compile测试一下

 ~/workspace/test  go tool compile -m main.go
main.go:3:6: can inline main
main.go:14:3: moved to heap: c

果然,在编译的时候,c 被编译器判定为逃逸变量,将c 放在堆中开辟

内联: go编译器会对一些小函数进行内联优化,以提升性能。内联优化意味着函数的代码会在调用处直接展开,而不是常规的函数调用。这就导致一些逃逸分析的行为发生变化,类似上面那个代码的内存地址就会是连续的。

什么时候编译器会进行内联优化?

  • 函数体较小:Go编译器更容易将体积较小的函数进行内联
  • 无复杂控制结构:如果函数内没有复杂的循环,条件分支等……内联的可能性更高
  • 函数参数和返回值简单:函数参数和返回值不过于复杂也有助于函数的内联

二、new的变量内存分配在栈还是堆上?

new 出来的变量,内存一定是分配在堆上吗?

还是原来的代码,我们通过new 分开来看看:

package main

func main() {
	// 打印返回的指针地址
	println(fool())
	// 0x1400001a0a0
}

//go:noinline    内置的编译指令,可以强制让 Go 编译器不对指定的函数进行内联优化
func fool() *int {
	var (
		a = new(int)
		b = new(int)
		c = new(int)
		d = new(int)
		e = new(int)
	)
	println(a, b, c, d, e)
	// 0x14000098f38 0x14000098f30 0x1400001a0a0 0x14000098f28 0x14000098f20
	return c
}

很明显,c 的地址 0x1400001a0a0 依然和其他的不是连续的内存空间,依然具备逃逸行为。所以这里不是分配在堆上的。

结论:

  • new 并不强制堆分配: 使用 new (T)分配的变量不一定分配在堆上,依然依赖于 Go 编译器的逃逸分析结果。
  • 是否逃逸决定了内存分配的位置:如果变量需要在函数作用域外使用(逃逸),则分配在堆上;如果可以在局部栈中管理,则分配在栈上。

三、逃逸规则

一般我们给一个引用类对象中的引用类成员进行赋值,就可能会出现逃逸现象。可以理解为访问一个引用对象实际上底层就是通过一个指针来间接的访问了,但是如果再访问里面的引用成员就会有第二次间接访问,这样操作这部分对象的话,就有可能会出现逃逸现象了。

Go 语言的引用类型有:func(函数类型)、 interface(接口类型)slice(切片类型)、 map(字典类型)、 channel(管道类型)、 *(指针类型) 等.

案例1

如果一个函数作为值传递给另一个函数,或者被作为闭包使用,生命周期超出其原始作用域,则它会逃逸。

package main

func main() {
	foo()()
}

//go:noinline
func foo() func() {
	return func() {
		println("call")
	}
}

通过编译看看逃逸分析:

~/workspace/test  go tool compile -m main.go
main.go:9:9: can inline foo.func1
main.go:9:9: func literal escapes to heap

能看到 发生了逃逸现象

案例2

对一个[]interface{} 类型尝试进行赋值,必定出现逃逸

package main

//go:noinline
func main() {
	var a = []interface{}{"100", "1000"}
	a[0] = 10
}

逃逸分析:

 ~/workspace/test  go tool compile -m main.go
main.go:5:23: []interface {}{...} does not escape
main.go:5:24: "100" does not escape
main.go:5:31: "1000" does not escape
main.go:6:2: 10 escapes to heap

a[0]=10 发生了逃逸现象

案例3

map[string]interface{}类型尝试通过赋值,必定出现逃逸

package main

//go:noinline
func main() {
	var a = make(map[string]interface{})
	a["hello"] = "world"
	a["1"] = "1"
}

逃逸分析:

 ~/workspace/test  go tool compile -m main.go
main.go:5:14: make(map[string]interface {}) does not escape
main.go:6:2: "world" escapes to heap
main.go:7:2: "1" escapes to heap

a["hello"] = "world" a["1"] = "1" 分别都发生了逃逸

案例4

map[interface{}]interface{} 类型尝试通过赋值,会导致key 和 value 的赋值出现逃逸

package main

//go:noinline
func main() {
	var a = make(map[interface{}]interface{})
	a["hello"] = "world"
}

看看编译结果:

 ~/workspace/test  go tool compile -m main.go
main.go:5:14: make(map[interface {}]interface {}) does not escape
main.go:6:2: "hello" escapes to heap
main.go:6:2: "world" escapes to heap

我们能看到,key 和 value 均发生了逃逸

案例5

map[string][]string数据类型,赋值 []string 会发生逃逸

package main

//go:noinline
func main() {
	var a = make(map[string][]string)
	a["hello"] = []string{"word1"}
}

通过逃逸分析发现:

 ~/workspace/test  go tool compile -m main.go
main.go:5:14: make(map[string][]string) does not escape
main.go:6:23: []string{...} escapes to heap

[]string{…} 切片发生了逃逸

案例6

[]*int 数据类型,赋值的右侧会发生逃逸

package main

//go:noinline
func main() {
	var a []*int
	var b = 3
	a = append(a, &b)
}

逃逸分析:

 ~/workspace/test  go tool compile -m main.go
main.go:6:6: moved to heap: b

其中 将 b 追加到 a 切片中, 最终 b 发生了逃逸

四、结论

golang 中的变量内存分配在堆上还是在栈上,是由编译器做逃逸分析之后决定的。

到此这篇关于golang内存逃逸分析的文章就介绍到这了,更多相关golang 内存逃逸内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang 实现struct、json、map互相转化

    golang 实现struct、json、map互相转化

    这篇文章主要介绍了golang 实现struct、json、map互相转化,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 破解IDEA(Goland)注册码设置 license server一直有效不过期的过程详解

    破解IDEA(Goland)注册码设置 license server一直有效不过期的过程详解

    这篇文章主要介绍了破解IDEA(Goland)注册码设置 license server一直有效不过期,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • 彻底理解golang中什么是nil

    彻底理解golang中什么是nil

    这篇文章主要介绍了golang中的nil用法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 使用Go实现健壮的内存型缓存的方法

    使用Go实现健壮的内存型缓存的方法

    这篇文章主要介绍了使用Go实现健壮的内存型缓存,本文比较了字节缓存和结构体缓存的优劣势,介绍了缓存穿透、缓存错误、缓存预热、缓存传输、故障转移、缓存淘汰等问题,并对一些常见的缓存库进行了基准测试,需要的朋友可以参考下
    2022-05-05
  • Golang实现将中文转化为拼音

    Golang实现将中文转化为拼音

    这篇文章主要为大家详细介绍了如何通过Golang实现将中文转化为拼音功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-02-02
  • 详解Golang中使用map时的注意问题

    详解Golang中使用map时的注意问题

    Golang中的map是一种数据结构,它允许你使用键值对的形式存储和访问数据,map在Go中是非排序的,提供了高效查找、插入和删除元素的能力,特别是当键是不可变类型,本文给大家详细介绍了Golang中使用map时的注意问题,需要的朋友可以参考下
    2024-06-06
  • Golang小数操作指南之判断小数点位数与四舍五入

    Golang小数操作指南之判断小数点位数与四舍五入

    这篇文章主要给大家介绍了关于Golang小数操作指南之判断小数点位数与四舍五入的相关资料,文中通过实例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2022-03-03
  • go语言搬砖之go jmespath实现查询json数据

    go语言搬砖之go jmespath实现查询json数据

    这篇文章主要为大家介绍了go语言搬砖之go jmespath实现查询json数据,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • Golang请求fasthttp实践

    Golang请求fasthttp实践

    本文主要介绍了Golang请求fasthttp实践,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • Go modules replace解决Go依赖引用问题

    Go modules replace解决Go依赖引用问题

    这篇文章主要为大家介绍了Go modules replace解决Go依赖引用问题,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06

最新评论