Golang内存逃逸

293 阅读2分钟

Go语言内存逃逸的几种情况:

  1. 在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,则溢出。
  2. 发送指针或带有指针的值到channel,因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放。
  3. 在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。
  4. 因为切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。
  5. 在interface类型上调用方法,在Interface调用方法是动态调度的,只有在运行时才知道。

用一句话总结上述情况:多级间接赋值容易导致逃逸

记住公式 Data.Field = Value,如果 Data, Field 都是引用类的数据类型,则会导致 Value 逃逸。这里的等号 = 不单单只赋值,也表示参数传递

  • []interface{}: data[0] = 100 会导致 100 逃逸
  • map[string]interface{}: data["key"] = "value" 会导致 "value" 逃逸
  • map[interface{}]interface{}: data["key"] = "value" 会导致 key 和 value 都逃逸
  • map[string][]string: data["key"] = []string{"hello"} 会导致切片逃逸
  • map[string]*int: 赋值时 *int 会 逃逸
  • []*int: data[0] = &i 会使 i 逃逸
  • func(*int): data(&i) 会使 i 逃逸
  • func([]string): data([]{"hello"}) 会使 []string{"hello"} 逃逸
  • chan []string: data <- []string{"hello"} 会使 []string{"hello"} 逃逸

避免内存逃逸

  • go语言的接口类型方法调用是动态,因此不能在编译阶段确定,所有类型结构转换成接口的过程会涉及到内存逃逸发生,在频次访问较高的函数尽量调用接口。
  • 不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。
  • 预先设定好slice长度,避免频繁超出容量,重新分配。
  • 对于每个结构体,把它看作纯值或纯指针,压根就不去使用&这种取地址的操作,避免隐式的内存分配

逃逸分析

概念:go在编译时进行逃逸分析,它会决定一个对象放在栈上还是堆上,不逃逸的放栈上,可能逃逸的放堆上

目的:尽可能将变量分配到栈上

方式: 编译器可以证明变量在函数返回后不再被引用,才会分配到栈上,否则分配到堆上

逃逸机制:编译器会根据变量是否被外部引用来决定是否逃逸

  1. 函数没有外部引用,优先放到栈中
  2. 函数外部存在引用,放在堆中
  3. 栈上放不下,必定放到堆中

案例:

首先了解:

image.png

package main

type Ruleset []Rule

func (r Ruleset) Match(path string) (*Rule, error) {
    for i := len(r)-1; i >= 0; i-- {
        rule := r[i]
        match, err := rule.Match(path)
        if match || err != nil {
            return &rule, err
        }
    }
    return nil, nil
}

这个程序中存在的问题是,逃逸的变量是rule这个结构体

原因:rule存储在Ruleset这个切片里,很明显它存在于堆中,并且在给rule赋值的时候实际上是做了一个不必要的拷贝(struct是值类型),后面用“&”取地址时创建了一个逃逸的指针指向它的副本

改进后:

package main

type Ruleset []Rule

func (r Ruleset) Match(path string) (*Rule, error) {
    for i := len(r)-1; i >= 0; i-- {
        rule := &r[i]
        match, err := rule.Match(path)
        if match || err != nil {
            return rule, err
        }
    }
    return nil, nil
}

这样就引用了切片中的结构体,避免了拷贝