go堆栈逃逸分析垃圾回收

堆栈

一般堆栈

  1. 栈一般由操作系统来分配和释放,堆由程序员通过编程语言来申请创建与释放。
  2. 栈用来存放函数的参数、返回值、局部变量、函数调用时的临时上下文等,堆用来存放全局变量。我们可以这样理解数据存放的规则:只要是局部的、占用空间确定的数据,一般都存放在 stack 里面,否则就放在 heap 里面。
  3. 一般来说,每个线程分配一个 stack,每个进程分配一个 heap,也就是说,stack 是线程独占的,heap 是线程共用的。
  4. stack 创建的时候,大小是确定的,数据超过这个大小,就发生 stack overflow 错误,而 heap 的大小是不确定的,需要的话可以不断增加。
  5. 栈是由高地址向低地址增长的,而堆是由低地址向高地址增长的。

go 堆栈

==Go 是自己管理内存的,而不是交给操作系统,它每次从操作系统申请一大块内存,然后按照 Google 的 TCMalloc 算法进行内存分配,也划分为堆、栈等很多区域==

变量是放在堆还是栈?

只要有对变量的引用,变量就会存在,而它存储的位置与语言的语义无关。如果可能,变量会被分配到其函数的栈,但如果编译器无法证明函数返回之后变量是否仍然被引用,就必须在堆上分配该变量,采用垃圾回收机制进行管理,从而避免指针悬空。此外,局部变量如果非常大,也会存在堆上。

在编译器中,如果变量具有地址,就作为堆分配的候选,但如果逃逸分析可以确定其生存周期不会超过函数返回,就会分配在栈上。

总之,分配在堆还是栈完全由编译器确定。

逃逸分析

变量发生逃逸的情况可以总结如下

  1. 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。

    package main
    
    func ab() *int {
    	i := 2
    	return &i
    }
    
    func main() {
    	c := ab() // 返回指针被引用
    	println(c)
    }
    
    channel1.go:3:6: can inline ab
    channel1.go:8:6: can inline main
    channel1.go:9:9: inlining call to ab
    channel1.go:4:2: moved to heap: i
    
  2. 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。

    package main
    
    func main() {
    	ch := make(chan *int)
    	i := 2
    	go func() {
    		<-ch
    	}()
    	ch <- &i
    
    }
    
    channel1.go:6:5: can inline main.func1
    channel1.go:5:2: moved to heap: i
    channel1.go:6:5: func literal escapes to heap
    
    
  3. 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。

    package main
    
    func main() {
    	i := 2
    	var a []*int
    	a = append(a, &i)
    
    	println(a)
    
    }
    
    channel1.go:3:6: can inline main
    channel1.go:4:2: moved to heap: i
    
    
  4. slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。

    package main
    
    func main() {
    	a := make([]int, 0, 10000)
    
    	println(a)
    
    }
    
    channel1.go:3:6: can inline main
    channel1.go:4:11: make([]int, 0, 10000) escapes to heap
    
    
  5. 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片 b 的背后存储都逃逸掉,所以会在堆上分配。

    package main
    
    import "reflect"
    
    func name(first interface{}) {
    	reflect.TypeOf(first).Kind() // 编译器无法确定其具体的类型,因此会产生逃逸,最终分配到堆上。
    	println(first)
    }
    
    func main() {
    	name("nil")
    }
    
    channel1.go:6:16: inlining call to reflect.TypeOf
    channel1.go:6:16: inlining call to reflect.toType
    channel1.go:10:6: can inline main
    channel1.go:5:11: leaking param: first
    channel1.go:11:7: "nil" escapes to heap
    

垃圾回收

标记清除方式

  1. 标记:从根对象出发依次遍历对象的子对象并将从根节点可达的对象都标记成存活状态
  2. 清除:收集器会依次遍历堆中的所有对象,释放其中没有被标记的 对象并将新的空闲内存空间以链表的结构串联起来,方便内存分配器的使用

缺点:标记阶段结束后,垃圾收集器会依次遍历堆中的对象并清除其中的垃圾,整个过程需要标记对象的存活状态,用户程序在垃圾收集的过程中也不能执行,我们需要用到更复杂的机制来解决 STW 的问题

三色标记法

工作原理:

  1. 垃圾收集的根对象会被标记成灰色,垃圾收集器只会从灰色对象集合中取出对象开始扫描
  2. 从灰色对象的集合中选择一个灰色对象并将其标记成黑色;
  3. 将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收;
  4. 重复上述两个步骤直到对象图中不存在灰色对象;
  5. 我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾

当标记的同时,用户修改了标记为垃圾的引用,会导致对象被删除,引发问题。所以引入了屏障技术,参考