简介
在Go中堆内存的分配和释放不需要去手动申请和释放,Go语言引入了GC回收的机制,GC机制会对位于堆上的对象进行自动管理,当某个对象不可达时(既没有其对象引用它时),它将会被回收并被重用。引入GC机制可以让开发者降低对内存管理的心智压力
堆和栈区别:
- 堆
- Go中全局的堆(heap)空间用来动态分配内存,有GC机制,开销较大
- 栈
- goroutine中的栈(stack)、局部变量的栈空间,开销很小
内存逃逸
在程序中,每个函数块都有自己的内存区域存放自己的局部变量(内存占用少)、返回地址、返回值等数据,这一块内存区域有特定的结构和寻址方式,寻址起来十分迅速,开销很少。这一块内存地址也称为栈。栈是线程级别,大小在创建时候已经确定。当变量太大时,会“逃逸”到堆上,这种现象称为内存逃逸。简单地说,局部变量通过堆分配和回收,就叫内存逃逸。
内存逃逸危害
堆是一块没有特定结构,也没有固定大小的内存区域,可以根据需要进行调整。全局变量,内存占用较大的局部变量,函数调用结束后不能立刻回收的局部变量都会存在堆里面。变量在堆上的分配和回收都比在栈上开销大的多。对于 Go 这种带 GC 的语言来说,会增加 GC 压力,同时也容易造成内存碎片。
避免内存逃逸
- 对于比较小型的数据,尽量传值而不是传指针,避免内存逃逸
- 避免使用长度不固定的slice切片,在编译期无法确定切片长度,只能将切片使用堆分配
- interface调用方法会发生内存逃逸,在热点代码片段,谨慎使用
分析内存逃逸发生
指令
- go build -gcflags=-m main.go
- go tool compile -m main.go
好处
- 通过逃逸分析,可以确认特定变量是分配在堆内存还是栈内存,提高了程序性能。
- 降低GC压力,不逸出的变量可以及时回收。
逃逸例子
example1:指针逃逸
package main
type Person struct {
Name string
}
func SetPerson(name string) *Person {
//new(Person) escapes to heap 这里发生了逃逸
p := new(Person)
p.Name = name
return p
}
func main() {
SetPerson("reggie")
}
example2:堆栈空间不足逃逸
func main() {
//10000的空间开销超出了最大的8192栈空间,逃逸到堆上
//main.go:30:11: make([]int, 10000, 10000) escapes to heap
s := make([]int, 10000, 10000)
for index, _ := range s {
s[index] = index
}
}
通常64位的计算机栈大小是8MB,可以通过以下指令通过stack size查看
- 指令
- ulimit -a
example3: 动态类型逃逸
package main
import "fmt"
func main() {
//发生逃逸
fmt.Println("hello golang")
}
参数类型为… interface{} ,在编译时很难确定参数的具体类型,这种情况也会产生逃逸。内置的Println方法中引用了interface{}