09.内存逃逸
# 01.内存逃逸
# 1、其他语言内存回收机制
- 在C/C++开发中,动态分配内存(new/malloc)需要我们手动释放资源
- 这样做的好处是,需要申请多少内存空间可以很好的掌握怎么分配
- 但是这有个缺点,如果忘记释放内存,则会导致内存泄漏
- 在很多高级语言中(python/Go/java)都加上了垃圾回收机制
# 2、什么是内存逃逸
- 函数内部申请的临时变量,正常会
分配到栈里
,栈中的内存分配非常快,自动回收,无需垃圾回收
- 但是若果申请的
临时变量作为了函数返回值
,编译器会认为在退出函数之后还有其他地方在引用
- 在编译的时候就会将
变量存储到堆中
,堆中的数据不会自动回收,必须使用垃圾回收机制
清除 我们将这种 由于某些原因,数据没有分配到栈中而是分配到堆中的现象叫做 内存逃逸
# 3、内存分配堆和栈
# 1)内存分片概述
Go的垃圾回收,让堆和栈堆程序员保持透明
真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写
把那些内存管理的复杂机制交给编译器
栈 可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统
# 2)分配到栈里
- 下面的例子,函数内部申请的临时变量,即使你是用make申请到的内存
- 如果发现在退出函数后没有用了,那么就把丢到栈上,毕竟栈上的内存分配比堆上快很多
func F() {
temp := make([]int, 0, 20)
...
}
1
2
3
4
2
3
4
# 3)分配到堆里
- 申请的代码和上面的一模一样,但是申请后作为返回值返回了
- 编译器会认为在退出函数之后还有其他地方在引用,当函数返回之后并不会将其内存归还
- 那么就申请到堆里
func F() {
temp := make([]int, 0, 20)
...
return temp
}
1
2
3
4
5
2
3
4
5
# 4)分配到栈里和堆里区别
分片到堆的坏处
- 如果变量都分配到堆上,堆不像栈可以自动清理
- 它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销
放到堆还是栈的情况
- 堆适合不可预知的大小的内存分配
- 但是为此付出的代价是分配速度较慢,而且会形成内存碎片
- 栈内存分配则会非常快,栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”分配和释放;
- 而堆分配内存首先需要去找到一块大小合适的内存块之后要通过垃圾回收才能释放
# 02.逃逸的几种情况
# 0、逃逸分析
- 逃逸分析是分析在程序的哪些地方可以访问到该指针
- 简单来说,
编译器会根据变量是否被外部引用来决定是否逃逸
1、如果函数外部没有引用,则优先放到栈中;
2、如果函数外部存在引用,则必定放到堆中;
1
2
2
对此你可以理解为,逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为
注意:
go 在编译阶段确立逃逸,并不是在运行时
# 1、指针逃逸
方法内把局部变量指针返回
提问:函数传递指针真的比传值效率高吗?
我们知道传递指针可以减少底层值的拷贝,可以提高效率
但是如果拷贝的数据量小,由于指针传递会产生逃逸,逃逸可能存储到堆中
存储到堆可能会增加GC的负担,所以传递指针不一定是高效的
# 2、栈空间不足逃逸
- 当我们创建一个切片长度为10000时就会逃逸
- 实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中
- slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )
- slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配
- 如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配
go build -gcflags=-m
1
package main
import "fmt"
func main() {
s := make([]int, 10000, 10000)
fmt.Println(s)
}
1
2
3
4
5
6
7
2
3
4
5
6
7
# 3、动态类型逃逸
- 很多函数参数为interface类型,比如 Println函数
func Println(a ...interface{}) (n int, err error)
1
- 编译期间很难确定其参数的具体类型,也能产生逃逸
# 03.如何避免
# 1、避免返回指针
func createSlice() *[]int {
slice := []int{1, 2, 3}
return &slice // 指向局部变量,逃逸到堆上
}
func createSlice() []int {
return []int{1, 2, 3} // 不需要逃逸到堆上
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 2、使用值接收者
func (s *LargeStruct) Process() {
// 方法会修改结构体
}
//推荐:如果结构体较小且方法不需要修改其状态,使用值接收者
func (s SmallStruct) Process() {
// 方法不会修改结构体
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 3、避免在闭包中引用外部变量
- 闭包会捕获其创建环境中的变量,这可能导致这些变量逃逸到堆上
- 尽量减少闭包的使用,或者将闭包限制在最小范围内
func main() {
x := 10
f := func() int {
return x // 闭包捕获外部变量 x
}
fmt.Println(f())
}
//推荐:将闭包中的变量作为参数传递
func createClosure(x int) func() int {
return func() int {
return x
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4、使用局部变量
- 在函数中,尽量将变量声明为局部变量
func createBuffer() *bytes.Buffer {
buffer := new(bytes.Buffer) // 临时对象,逃逸到堆上
return buffer
}
//推荐:使用局部变量
func createBuffer() bytes.Buffer {
return bytes.Buffer{} // 使用局部变量
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
上次更新: 2024/10/15 16:27:13