Golang | 每日一练 (6)
💢欢迎来到张胤尘的技术站
💥技术如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥
文章目录
- Golang | 每日一练 (6)
- 题目
- 参考答案
- 什么是内存逃逸?
- 内存逃逸对程序有什么样的影响?
- 如何避免?
Golang | 每日一练 (6)
题目
什么是内存逃逸?内存逃逸对程序有什么样的影响?如何避免?
参考答案
什么是内存逃逸?
内存逃逸是指在函数内部创建的变量或对象,在函数结束后仍然被其他部分引用或持有,从而脱离了函数的作用域。在 golang
中,编译器会根据逃逸分析的结果,将变量分配到栈或堆上。如果变量的生命周期超出了函数的作用域,就会被分配到堆上,这种现象被称为内存逃逸。
例如:
package main
import (
"fmt"
)
func createSlice() []int {
var s []int
for i := 0; i < 10; i++ {
s = append(s, i)
}
return s
}
func main() {
slice := createSlice()
fmt.Println(slice)
}
在上述代码中,在 createSlice
函数的内部创建了一个局部变量 s
,并在循环中向其中添加了元素。最后,函数返回了这个切片。在正常情况下,这个局部变量 s
会被分配到函数栈上,但是逃逸分析会检测到切片 s
的底层数组需要在函数外被访问,因此会将底层数组分配到堆上,而不是栈上。
下面通过 go build
命令可以更加直观的看出内存逃逸的表现。如下所示:
$ go build -gcflags="-m" test.go
# command-line-arguments
./test.go:7:6: can inline createSlice
./test.go:16:22: inlining call to createSlice
./test.go:17:13: inlining call to fmt.Println
./test.go:17:13: ... argument does not escape
./test.go:17:14: slice escapes to heap
在以上的输出结果中,slice escapes to heap
这是关键信息,表明 createSlice
函数返回的切片 slice
被分配到了堆上。
内存逃逸对程序有什么样的影响?
内存逃逸对程序的影响主要体现在以下几个方面:
- 堆分配的持久性:当变量逃逸到堆上时,它们的生命周期不再受局部作用域的限制,而是由垃圾回收器管理。这意味着这些变量在使用完毕后不会立即释放,而是等待
gc
的回收。因此,程序的堆内存占用会增加。 - 内存碎片化:频繁的堆分配和释放可能导致内存碎片化,降低内存的利用率。尤其是在长时间运行的程序中,内存碎片化可能导致可用内存减少,影响程序的性能。
- 堆分配的开销:堆分配比栈分配更复杂且耗时。栈的分配和访问只需要通过移动栈顶指针即可;而堆分配需要动态管理内存,可能会涉及复杂的内存分配算法和同步机制。
- 垃圾回收的负担:堆上的内存需要由
gc
管理。当堆内存增加时,gc
的运行频率也会增加,这会导致程序的STW
或额外的性能开销。频繁的gc
可能会显著降低程序的响应速度和吞吐量。 - 引用计数问题:堆上的对象可能被多个引用持有,这使得生命周期管理变得复杂。例如,一个对象可能被意外地保留,导致内存泄漏。
- 悬挂指针风险:如果堆上的对象被释放,但仍有指针指向它,可能会导致悬空指针问题,进而引发程序崩溃或未定义行为。
悬空指针是指指针曾经指向一个有效的内存位置,但该内存已被释放或回收,导致指针变得无效。尽管指针仍然保存着原来的地址,但访问该地址会产生未定义行为,因为该地址可能已经被分配给其他对象或成为不可访问的区域。
总的来说,内存逃逸对程序的影响是多方面的,既有积极的一面,也有消极的一面。在实际开发中,合理管理内存逃逸是优化程序性能和稳定性的关键。以下是一些优化建议:
- 减少不必要的堆分配:尽量使用栈分配(如局部变量)或预分配内存,减少堆分配的频率。
- 优化数据结构:选择合适的数据结构,避免不必要的内存逃逸。
- 使用工具分析:利用
golang
的逃逸分析工具和性能分析工具,找出并优化不必要的内存逃逸。 - 合理使用堆分配:在需要动态内存或跨作用域共享时,合理使用堆分配,避免过度优化。
如何避免?
在之前的代码中,由于这个场景中切片的逃逸是不可避免的(因为需要返回切片),但可以通过以下方式减少不必要的堆分配:
package main
import (
"fmt"
)
func createSlice() []int {
// 预分配足够的容量,避免多次扩容
s := make([]int, 10)
for i := 0; i < 10; i++ {
s[i] = i
}
return s
}
func main() {
slice := createSlice()
fmt.Println(slice)
}
- 预分配容量:使用
make([]int, 10)
预分配切片的容量,避免在append
过程中多次扩容。扩容会导致额外的堆分配和数据拷贝。这种优化可以减少堆分配的频率和大小,但无法完全避免切片的逃逸。 - 减少不必要的引用:如果切片的生命周期较短,可以考虑在函数内部直接操作切片,而不是返回它。但这取决于具体需求。
🌺🌺🌺撒花!
如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。