语言基础

Go 内存逃逸

内存逃逸 #

可以通过 go build -gcflags=-m main.go 分析内存逃逸

编译阶段不能确定大小的变量以及生命周期超出函数的局部变量数据都会逃逸到堆中。

1. 指针逃逸 #

  1. 函数返回指向局部变量的指针,变量内存不能随着函数结束而回收,只能分配在堆上
  2. 函数调用其他寿命更长的函数时,将局部变量指针传递过去,同理。

2. 将变量存储到 interface{} 变量中 #

TODO 如果函数参数为 interface{},编译期间很难确定其参数的类型以及大小,也会发生逃逸。

https://goperf.dev/01-common-patterns/interface-boxing/

3. 栈空间不足 #

每个 goroutine 都维护着一个自己的栈区,初始大小 2KB,栈结构经过了分段栈到连续栈的发展。

分配大变量,如大 slice,或者大小不确定的变量,会有可能栈空间不足,然后编译器将其分配在堆上,虽然栈会自动增长,但是也有大小限制(TODO)。

4. 闭包捕获变量 #

当一个闭包函数引用了外部变量并且会执行后续读写操作,则变量会被逃逸到堆上。

package main

func Increase() func() int {
	n := 0 // move to heap
	return func() int {
		n++
		return n
	}
}

func main() {
	in := Increase()
	println(in()) // 1
}

5. 变量地址存储在引用类型对象中 #

如将变量地址保存在 切片(slice)、映射(map)、通道(channel)、 接口(interface) 以及 函数(func) 中,那么此变量会逃逸到堆上.

...

Go GC 垃圾回收

GC 垃圾回收 #

分配在栈上的数据,随着函数调用栈的销毁便释放了自身占用的内存,可以被程序重复利用。

协程栈也是从堆上分配的,也在 mheap 管理的 span 中,mspan.spanState 会记录该 span 是用作堆内存还是栈内存。

而分配在堆上的数据,他们占用的内存需要程序主动释放才可以重新使用,否则称为垃圾。

三色标记原理 #

三色标记法,白色,灰色和黑色

  1. 垃圾回收开始会把所有数据(栈、堆、数据段)都标记为白色
  2. 然后把直接追踪(扫描全局数据区和栈区)到的 root 节点标记为灰色,灰色代表基于当前节点展开的追踪还未完成。
  3. 基于某个节点的追踪任务完成后标记为黑色,标识有用并且无需基于它再追踪。
  4. 没有灰色节点后意味着标记工作结束。此时有用的数据为黑色,垃圾都是白色,在清除阶段回收这些白色的垃圾即可。

混合写屏障 #

通过 混合写屏障 防止GC过程中并发修改对象的问题。

  • 混合写屏障 继承了插入写屏障的优点,起始时无需 STW 打快照,直接并发扫描垃圾即可
  • 混合写屏障 继承了删除写屏障的优点,赋值器是黑色赋值器,GC期间,任何在栈上创建的新对象,均为黑色。扫描过后就不需要扫描了,这样就消除了插入写屏障最后 STW 的重新扫描栈了。
  • 混合写屏障 扫描栈虽然不用 STW,但是扫描某一个具体的栈的时候,还是要停止这个 goroutine 赋值器的工作(针对一个 goroutine 来说,是暂停扫的,要么全灰,要么全黑,是原子状态切换的)

GC 触发时机 #

  1. 主动触发:调用 runtime.GC
  2. 被动触发:使用系统监控 sysmon,该触发条件由 runtime.forcegcperiod 控制,默认为 2 分钟。当超过时间没有产生任何 GC 时,强制触发 GC。使用步调算法。。。

GC 流程 #

Go GC: Latency Problem Solved slide no 12

...

Go make 与 new 区别

make 和 new 的区别 #

都是内存分配函数

1. 基本用途 #

make 仅用于创建 slicemapchannel,并且会初始化这些类型的内部数据结构,返回初始化后的值类型 new 可用于任何类型,返回指向该类型零值的指针

2. 返回值 #

make返回初始化后的值类型 new返回指向该类型零值的指针

3. 内存分配 #

make会分配内存并初始化数据结构 new只分配内存,并把内存置零,不做初始化

Go GMP 模型

CSP #

CSP(Communicating Sequential Processes) 被认为是 Go 在并发编程中成功的关键,论文指出应该重视 input 和 output 原语,尤其是并发编程的代码。

GMP介绍 #

G M P 是 Go 调度器的三个核心组件

G 对应 goroutine, 属于用户线程或绿色线程 #

type g struct {
	stack       stack   // goroutine 使用的栈,存储了栈的范围 [lo, hi)
	m           *m      // 当前与 g 绑定的 m
	sched       gobuf   // goroutine 的运行现场, 存储各种寄存器的值,如 PC、SP等寄存器,M恢复现场时需要用到
}

M 对应内核线程 #

M 代表一个工作线程或者说系统线程,G需要调度到M上才能执行,和 P 绑定去获取 G 来执行。

它保存了 M 自身使用的栈信息,当前正在M上执行的G信息,与之绑定的 P 信息。

// m 代表工作线程,保存了自身使用的栈信息
type m struct {
	// 记录工作线程(也就是内核线程)使用的栈信息。在执行调度代码时需要使用
	// 执行用户 goroutine 代码时,使用用户 goroutine 自己的栈,因此调度时会发生栈的切换
	g0      *g     // goroutine with scheduling stack/
	// 通过 tls 结构体实现 m 与工作线程的绑定
	// 这里是线程本地存储
	tls           [6]uintptr // thread-local storage (for x86 extern register)
	// 当前工作线程绑定的 p
	p             puintptr // attached p for executing go code (nil if not executing go code)
	// 工作线程 id
	thread        uintptr // thread handle
	// 记录所有工作线程的链表
	alllink       *m // on allm
}

P 是调度队列,包含缓存信息 #

P 取 processor 首字母,为 M 的执行提供上下文,保存 M 执行 G 时的一些资源,例如本地可执行 G 队列、memory cache等。一个M只有绑定P才可以执行goroutine,当M阻塞时,整个P会被传递给其他M,或者说整个P被接管。

...