简介

Go 程序会在两个地方为变量分配内存。一个是全局空间用来动态分配内存。另一个是每个Goroutine的空间。Go语言实现了垃圾回收机制,虽然不用担心内存是分配到栈空间,还是堆空间,但不同的分配方式,存在着不同的性能差异。

  • 在函数中申请一个对象,如果分配到栈中,函数执行结束后自动回收。如果分配到堆中,则在函数结束后某个时间点进行垃圾回收
  • 在栈上分配和回收内存的开销很低。在堆上分配内存,一个很大的额外开销则是垃圾回收。Go 语言使用的是标记清除算法,并且在此基础上使用了三色标记法和写屏障技术,提高了效率。
  • Go 编译器确定某个变量是分配在栈上还是堆上的过程,被称为逃逸分析。逃逸分析由编译器完成,作用于编译阶段

逃逸出现情况

  1. 指针逃逸

    当函数中创建了一个对象,并返回了这个对象的地址。当函数退出时,因为指针的存在,对象的内存不能随着函数结束而回收,因此对象只能逃逸分配到堆中。

     1package main
     2
     3import "fmt"
     4
     5func getPointer() *struct{} {
     6	a := new(struct{})
     7	return a
     8}
     9
    10func main() {
    11	p := getPointer()
    12	fmt.Println(p)
    13}
    
     1-- 通过选项 -gcflags=-m,查看变量逃逸的情况
     2-- go build -gcflags=-m test.go
     3
     4$ go build -gcflags=-m test.go
     5# command-line-arguments
     6.\test.go:5:6: can inline getPointer
     7.\test.go:11:17: inlining call to getPointer   
     8.\test.go:12:13: inlining call to fmt.Println  
     9.\test.go:6:10: new(struct {}) escapes to heap 
    10.\test.go:11:17: new(struct {}) escapes to heap
    11.\test.go:12:13: ... argument does not escape  
    
  2. 闭包

    当函数内部的局部变量被匿名函数捕获并在函数外被调用时,这些局部变量就会逃逸到堆上

     1package main
     2
     3import "fmt"
     4
     5func closureEscape() func() int {
     6	x := 10
     7	// 返回一个闭包,该闭包捕获了外部函数的局部变量x
     8	return func() int {
     9		x += 5
    10		return x
    11	}
    12}
    13
    14func main() {
    15	// 调用closureEscape函数,并将返回的闭包赋值给变量f
    16	f := closureEscape()
    17
    18	// 在main函数外部,继续使用闭包f,这时闭包中的局部变量x就逃逸到堆上
    19	fmt.Println(f()) // 输出:15
    20	fmt.Println(f()) // 输出:20
    21}
    
     1-- 通过选项 -gcflags=-m,查看变量逃逸的情况
     2-- go build -gcflags=-m test.go
     3
     4$ go build -gcflags=-m test.go
     5# command-line-arguments
     6.\test.go:5:6: can inline closureEscape        
     7.\test.go:8:9: can inline closureEscape.func1  
     8.\test.go:16:20: inlining call to closureEscape
     9.\test.go:8:9: can inline main.func1
    10.\test.go:19:15: inlining call to main.func1   
    11.\test.go:19:13: inlining call to fmt.Println  
    12.\test.go:20:15: inlining call to main.func1   
    13.\test.go:20:13: inlining call to fmt.Println  
    14.\test.go:6:2: moved to heap: x
    15.\test.go:8:9: func literal escapes to heap    
    16.\test.go:16:20: func literal does not escape  
    17.\test.go:19:13: ... argument does not escape  
    18.\test.go:19:15: ~R0 escapes to heap
    19.\test.go:20:13: ... argument does not escape  
    20.\test.go:20:15: ~R0 escapes to heap
    
  3. 变量占用内存较大 / 大小不确定

    由于栈所能分配的空间有限,当分配的内存超过了Go 编译器的阈值时,所分配的内存就会逃逸到堆上。

    同时,如果分配的大小不确定,编译器不确定后续是否是大内存,此时也将变量在堆上分配。

     1package main
     2
     3import "math/rand"
     4
     5func genLowM() {
     6	nums := make([]int, 10)
     7	for i := 0; i < 10; i++ {
     8		nums[i] = rand.Int()
     9	}
    10}
    11
    12func genHigtM() {
    13	nums := make([]int, 10000)
    14	for i := 0; i < 10000; i++ {
    15		nums[i] = rand.Int()
    16	}
    17}
    18
    19func generate(n int) {
    20	nums := make([]int, n) // 不确定大小
    21	for i := 0; i < n; i++ {
    22		nums[i] = rand.Int()
    23	}
    24}
    25
    26func main() {
    27	genLowM()
    28	genHigtM()
    29	generate(1)
    30}
    
    1$ go build -gcflags=-m test.go
    2# command-line-arguments
    3.\test.go:6:14: make([]int, 10) does not escape    
    4.\test.go:13:14: make([]int, 10000) escapes to heap
    5.\test.go:20:14: make([]int, n) escapes to heap    
    
  4. 动态类型(interface{})逃逸

    如果变量是interface{}类型,编译期无法确定其具体的参数类型,所以内存分配到堆中。

     1package main
     2
     3import (
     4	"fmt"
     5)
     6
     7func main() {
     8	a := "1"
     9	fmt.Println(a)
    10}
    11
    12// fmt.Println() 的参数是interface{}类型
    
    1$ go build -gcflags=-m test.go
    2# command-line-arguments
    3.\test.go:9:13: inlining call to fmt.Println
    4.\test.go:9:13: ... argument does not escape
    5.\test.go:9:13: a escapes to heap
    

如何利用逃逸分析提高性能

​ 在函数参数传递时,传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆上,增加垃圾回收(GC)的负担。如果当对象频繁创建和删除时,传递指针会导致GC的开销影响性能。

对于需要修改原对象值,或占用较大内存的对象,需要选择传递指针。

对于只读且占用内存较小的对象,直接传值可以获得更好的性能