栈和堆
Golang 程序会在两个地方分配内存:每一个 goroutine 的栈和全局堆。但是垃圾回收机制在栈和堆上的性能差异非常大:
- 在栈上分配时,函数执行结束时自动回收。在栈上分配和回收内存的开销很低,只需要 2 个 CPU 指令:PUSH 和 POP。
- 在堆上分配时,在某个时间点上利用垃圾回收机制进行回收。在堆上分配内存,一个很大的额外开销则是垃圾回收,需要经过垃圾回收算法的计算才能回收堆上的对象。
逃逸分析
在函数内部声明变量时,需要知道这个变量分配在栈上还是堆上。
编译器决定内存分配位置的方式,就称之为逃逸分析。逃逸分析由编译器完成,作用于编译阶段。
发生场景
发生逃逸分析的场景:
- 指针逃逸
- interface{} 动态类型逃逸
- 栈空间不足
- 闭包
指针逃逸
这是最常见,也是最好理解的情况。
当函数返回指针时,这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,这个指针所指向的变量就需要分配在堆空间上。
interface{} 逃逸
Golang 中空接口可以表示任意类型,如果函数的参数类型为空接口类型,因为编译期间很难确定其参数的具体类型(是否是指针还未知),也会发生逃逸。
栈空间不足
操作系统对内核线程使用的栈空间是有大小限制的,当 goroutine 被调度时会绑定一个内核级线程去执行,所以 goroutine 的栈空间大小不能超过操作系统的限制。
对 Go 编译器而言,超过一定大小(或者无法确定大小,如切片)的局部变量将逃逸到堆上。
闭包
因为闭包函数可以引用函数体之外的变量,所以在闭包函数内引用的变量也会逃逸分析到堆上。
如:Increase 函数内部有一个闭包函数引用了外部变量 n,所以 n 不能因为闭包函数的退出而被销毁,所以 n 被分配在了堆上。当 in 对象被销毁时,n 才会被销毁。
1 | func Increase() func() int { |
传值 vs 传指针
传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收的负担。
一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。