GC
# 1.编译
# 1.1.逃逸分析
- 编译原理中,分析指针动态范围的方法称之为逃逸分析,当一个对象的指针被多个方法或线程引用时,称这个指针发生了逃逸。
Go语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化,优化结果可以决定一个变量分配到对上还是内存。Go语言逃逸分析最基本的原则是,如果一个函数返回对一个变量的引用,它就会发生逃逸。简单来说,编译器会分析代码的特性和代码声明周期,变量只有在编译器证明在函数返回后不会再被引用才分配到栈上,其他情况都是分配带堆上。 
# 1.2.变量逃逸
变量逃逸包含多种情况,指针逃逸属于最简单的一种,即函数中创建一个对象,返回这个对象指针。此时函数虽然退出了,但因为指针的存在,对象内存不能随函数结束而回收,因此会分配到
heap。type Demo struct { name string } func createDemo(name string) *Demo { d := new(Demo) // 局部变量d逃逸到堆 d.name = name return d } func main() { demo := createDemo("demo") fmt.Println(demo) }1
2
3
4
5
6
7
8
9
10
11
12
13
14上述示例中,
createDemo()函数的局部变量d发生逃逸,其引用在外部继续使用,内存分配到堆上随GC回收。编译时,可以借助-gcflags=-m选项查看变量逃逸情况。$ go build -gcflags=-m main_pointer.go ./main_pointer.go:10:6: can inline createDemo ./main_pointer.go:17:20: inlining call to createDemo ./main_pointer.go:18:13: inlining call to fmt.Println ./main_pointer.go:10:17: leaking param: name ./main_pointer.go:11:10: new(Demo) escapes to heap ./main_pointer.go:17:20: new(Demo) escapes to heap ./main_pointer.go:18:13: demo escapes to heap ./main_pointer.go:18:13: main []interface {} literal does not escape ./main_pointer.go:18:13: io.Writer(os.Stdout) escapes to heap <autogenerated>:1: (*File).close .this does not escape1
2
3
4
5
6
7
8
9
10
11接口动态类型逃逸属于另一种情况,由于空接口可以表示任意类型,作为函数参数,编译期间很难确定参数的具体类型,也会发生逃逸。
func main() { demo := createDemo("demo") fmt.Println(demo) } ./main_pointer.go:18:13: demo escapes to heap1
2
3
4
5
6demo是main函数中的一个局部变量,该变量作为实参传递给fmt.Println(),由于fmt.Println()的参数类型定义为interface{},因此也发生了逃逸。func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) }1
2
3此外,操作系统对内核线程使用的
stack大小有限,Go运行时(runtime)会尝试在goroutine需要的时候动态地分配栈空间,goroutine地初始栈大小为2KB。当goroutine被调度时,会绑定内核线程执行,栈空间的大小不会超过操作系统限制。当局部变量占用内存过大时,对象内存会分配到堆上。func generate8191() { nums := make([]int, 8191) // < 64KB for i := 0; i < 8191; i++ { nums[i] = rand.Int() } } func generate8192() { nums := make([]int, 8192) // = 64KB for i := 0; i < 8192; i++ { nums[i] = rand.Int() } } func generate(n int) { nums := make([]int, n) // 不确定大小 for i := 0; i < n; i++ { nums[i] = rand.Int() } } func main() { generate8191() generate8192() generate(1) }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26例如上述创建
int型切片示例,当切片占用内存小于最大限制时,局部变量会分配到栈上,但超出栈空间后,会逃逸到堆上。
# 1.3.闭包
一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包,闭包可以在一个内存函数访问其外层函数的作用域。
func Increase() func() int { n := 0 return func() int { n++ return n } } func main() { in := Increase() fmt.Println(in()) // 1 fmt.Println(in()) // 2 }1
2
3
4
5
6
7
8
9
10
11
12
13Increase返回值是一个闭包函数,该闭包函数访问外部变量n,变量n会一直存在,直到in被销毁,所以n会逃逸到堆上,直至没有被引用。
# 1.4.编译链接
编译过程就是对源文件进行词法分析、语法分析、语义分析、优化,最后生成汇编代码文件,以
.s作为文件后缀。最终汇编器会将汇编代码转变为机器可以执行的指令。编译器是将高级语言翻译成机器语言的重要工具,编译会拆分成扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化几部分。
语法分析会将字符序列转换为标记序列,
.go文件输入到扫描器,使用类似于有限状态机的算法将源代码的字符系列分割成一系列符号。扫描器位于src/cmd/compile/internal/syntax/scanner.go,最关键的是next函数,它不断读取下一个字符,直到字符构成一个Token。func (s *scanner) next() { ... redo: // skip white space c := s.getr() for c == ' ' || c == '\t' || c == '\n' && !nlsemi || c == '\r' { c = s.getr() } // token start s.line, s.col = s.source.line0, s.source.col0 if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) { s.ident() return } switch c { ... case '\n': s.lit = "newline" s.tok = _Semi case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': s.number(c) ... default: s.tok = 0 s.error(fmt.Sprintf("invalid character %#U", c)) goto redo return assignop: if c == '=' { s.tok = _AssignOp return } s.ungetr() s.tok = _Operator }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44代码主要逻辑就是通过
c=s.getr()获取下一个未被解析的字符,通过匹配不同情形解析出一个Token。生成Token序列后,需要经过进一步处理,生成一颗以表达式为节点的语法树。
编译器所能检查的是静态语义,它会根据变量类型进行匹配、转换,最终将类型标注到每个节点上。

编译器会在以上阶段检查常量、类型、函数声明以及变量赋值语句类型,同时检查哈希表中键的类型。类型检查处于编译的第二阶段,语法分析得到的抽象树会进行类型检查及校验,这一过程可能会改写抽象语法树,去除不会被执行的代码提高效率,修改
make、new等关键字对应节点的操作类型。抽象语法树处理完成后,语法树会进一步转换成中间代码,它是语法树的顺序表示。中间代码一般和目标机器及运行环节无关,常见的有三地址码、P-代码形式。Go中的中间代码会表示为SSA——Static Single-Assignment,静态单赋值,之所以称为单赋值,是因为每个名字在SSA中仅被赋值一次。这一阶段会根据CPU的结构设置相应的用于生成中间代码的变量,例如编译器使用的指针和寄存器大小、可用寄存器列表等,中间代码生成和机器码生成会共享相同的设置。此外,生成中间代码前会对抽象语法树中节点的一些元素进行替换。
对于
map的操作m[i]会被转换成mapacess和mapassign,中间代码的生成过程其实就是AST抽象树到SSA中间代码的转换过程,期间会对语法树中的关键字进行一次更新,更新后的语法树会经过多轮处理转变最后的SSA中间代码。最后,中间代码会转换成目标代码,并经由目标代码优化器对一些指令进行优化,如移位指令代替乘法指令。
# 1.5.程序启动过程
1.检查运行平台的CPU,设置好程序运行需要的相关标志 2.TLS初始化 3.runtime.args、runtime.osinit、runtime.schedinit三个方法做好程序运行需要的各种变量和调度器 4.runtime.newproc创建新的goroutine绑定用户写的main方法 5.runtime.mstart开始goroutine的调度1
2
3
4
5
main函数里执行的一些重要操作,涉及新建进程执行sysmon函数、定期垃圾回收和调度抢占,启动GC,执行所有init函数等。main函数执行结束后,会调用exit(0)退出进程,如果没有正常退出,main函数的重要代码会一直访问非法地址,系统会杀死进程,确保进程退出。
# 2.调度器
# 2.1.线程和协程
1.`goroutine`的栈内存消耗仅需`2kb`,栈空间不够时会自动进行扩容,但`Thread`需要`1MB`栈内存,还需要一个称为`a guard page`的区域实现线程的栈空间隔离。 2.对于`Thread`来说,由于关系到操作系统,属于内核级资源,创建和销毁伴随巨大消耗,一般会伴随线程池使用。`goroutine`则由`Go runtime`管理,创建和销毁的消耗极低,是用户级资源。 3.`Thread`切换需要保存各种寄存器用于恢复现场,`goroutines`一般只需要三个寄存器`Program Counter`、`Stack Pointer`、 `BP`。 4.线程切换一般消耗`1000-1500`纳秒,一个纳秒平均执行`12-18`条指令,相当于执行指令较少,`goroutines`切换仅需`200`纳秒,相当于损耗`2400-3600`条指令,成本比线程更低。1
2
3
4
# 2.2.scheduler
Go程序的执行由用户程序和运行时构成,它们之间通过函数调用实现内存管理、channel通信、goroutines创建等功能,用户程序进行的系统调用会被Runtime拦截,实现调度及GC相关工作。
Runtime维护所有的goroutines,通过scheduler进行调度。其中,goroutine和thread相互独立,但goroutine依赖thread执行,前者调度到线程执行只是runtime层面的概念,操作系统仅感知多线程。协程调度主要依赖三部分结构- G,用于保存协程状态、指令地址等现场
- M,内核线程,包含正在运行的协程等字段
- P,虚拟处理器,维护处于
runnable状态的G队列供M运行
Runtime起始时会启动一些垃圾回收的G、执行调度的G、运行代码的G,并通过一个M开始G的运行,随着G的增多,更多的M也会被创建出来。scheduler核心思想在于:复用线程、限制线程数、线程可以偷其他的协程也可以传递出自己的协程,调度器会启动一个后台线程sysmon,用来检测长时间(10ms)运行的协程,将其调度到global runqueues,该队列优先级最低,以示惩罚。
调度器是
Runtime的一部分,它属于用户空间资源,不同于操作系统调度的抢占式,Go调度器采用协作式,只是由Runtime来做,用户无法预测。
情形 说明 关键字 gogo创建一个新的goroutine,scheduler会考虑调度 GC 由于进行GC的 goroutine也需要在 M 上运行,因此肯定会发生调度。当然,Go scheduler还会做很多其他的调度,例如调度不涉及堆访问的goroutine来运行 系统调用 当goroutine进行系统调用时,会阻塞 M,所以它会被调度走,同时一个新的 goroutine 会被调度上来 内存同步访问 atomic,mutex,channel 操作等会使goroutine阻塞,因此会被调度走。等条件满足后还会被调度上来继续运行 
# 2.3.工作窃取
- 调度器的职责就是将所有处于
runnable的goroutines均匀分布到P上执行。当一个P发现LRQ已经没有G时,会从其他P偷一部分执行。 
scheduler的工作就是找到处于runnable的goroutines执行,直至被阻塞。当P2从LRQ依次获取G执行完毕后,会尝试从GRQ获取,如果都没有则随机选择其他P窃取G执行。
# 2.4.GPM
G用于保存协程的状态信息及CPU的寄存器值,便于协程被调度后恢复现场。协程被调离CPU时,CPU寄存器的值会保存在G对象的成员变量,调度起来时G对象保存的寄存器值会恢复到CPU寄存器。type g struct { // goroutine使用的栈 stack stack // offset known to runtime/cgo // 用于栈的扩张和收缩检查,抢占标志 stackguard0 uintptr // offset known to liblink stackguard1 uintptr // offset known to liblink _panic *_panic // innermost panic - offset known to liblink _defer *_defer // innermost defer // 当前与g绑定的m m *m // current m; offset known to arm liblink // goroutine 的运行现场 sched gobuf syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc stktopsp uintptr // expected sp at top of stack, to check in traceback // wakeup时传入的参数 param unsafe.Pointer // passed parameter on wakeup atomicstatus uint32 stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus goid int64 // g被阻塞之后的近似时间 waitsince int64 // approx time when the g become blocked // g被阻塞的原因 waitreason string // if status==Gwaiting // 指向全局队列里下一个g schedlink guintptr // 抢占调度标志,这个为true时,stackguard0等于stackpreempt preempt bool // preemption signal, duplicates stackguard0 = stackpreempt paniconfault bool // panic (instead of crash) on unexpected fault address preemptscan bool // preempted g does scan for gc gcscandone bool // g has scanned stack; protected by _Gscan bit in status gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove? throwsplit bool // must not split stack raceignore int8 // ignore race detection events sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine // syscall返回之后的cputicks,用来做tracing sysexitticks int64 // cputicks when syscall has returned (for tracing) traceseq uint64 // trace event sequencer tracelastp puintptr // last P emitted an event for this goroutine // 如果调用LockOsThread,那么这个g会绑定到某个m上 lockedm *m sig uint32 writebuf []byte sigcode0 uintptr sigcode1 uintptr sigpc uintptr // 创建该goroutine的语句的指令地址 gopc uintptr // pc of go statement that created this goroutine // goroutine函数的指令地址 startpc uintptr // pc of goroutine function racectx uintptr waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order cgoCtxt []uintptr // cgo traceback context labels unsafe.Pointer // profiler labels // time.Sleep缓存的定时器 timer *timer // cached timer for time.Sleep gcAssistBytes int64 }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61G的结构体关联了栈和指令寄存器,用于保存执行现场的信息。// 描述栈的数据结构,栈的范围:[lo, hi) type stack struct { // 栈顶,低地址 lo uintptr // 栈底,高地址 hi uintptr } type gobuf struct { // 存储rsp寄存器的值 sp uintptr // 存储rip寄存器的值 pc uintptr // 指向goroutine g guintptr ctxt unsafe.Pointer // this has to be a pointer so that gc scans it // 保存系统调用的返回值 ret sys.Uintreg lr uintptr bp uintptr // for GOEXPERIMENT=framepointer }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21M代表工作线程,G需要调度到M运行,结构体M保存自身使用的栈信息、G信息和绑定的P信息。当M无事可做时,会自旋尝试全局队列检查、network poller检查、GC甚至窃取工作。// m代表工作线程,保存自身使用的栈信息 type m struct { // 执行用户goroutine代码时,使用用户goroutine自己的栈,因此调度时会发生栈的切换 g0 *g // goroutine with scheduling stack/ morebuf gobuf // gobuf arg to morestack divmod uint32 // div/mod denominator for arm - known to liblink // Fields not known to debuggers. procid uint64 // for debuggers, but offset not hard-coded gsignal *g // signal-handling g sigmask sigset // storage for saved signal mask // 通过tls结构体实现m与工作线程的绑定,这里是线程本地存储 tls [6]uintptr // thread-local storage (for x86 extern register) mstartfn func() // 指向正在运行的goroutine对象 curg *g // current running goroutine caughtsig guintptr // goroutine running during fatal signal // 当前工作线程绑定的p p puintptr // attached p for executing go code (nil if not executing go code) nextp puintptr id int32 mallocing int32 throwing int32 // 该字段不等于空字符串的话,要保持curg始终在这个m上运行 preemptoff string // if != "", keep curg running on this m locks int32 softfloat int32 dying int32 profilehz int32 helpgc int32 // 为true时表示当前m处于自旋状态,正在从其他线程偷工作 spinning bool // m is out of work and is actively looking for work // m正阻塞在note上 blocked bool // m is blocked on a note // m正在执行write barrier inwb bool // m is executing a write barrier newSigstack bool // minit on C thread called sigaltstack printlock int8 // 正在执行cgo调用 incgo bool // m is executing a cgo call fastrand uint32 // cgo调用总计数 ncgocall uint64 // number of cgo calls in total ncgo int32 // number of cgo calls currently in progress cgoCallersUse uint32 // if non-zero, cgoCallers in use temporarily cgoCallers *cgoCallers // cgo traceback if crashing in cgo call // 没有goroutine需要运行时,工作线程睡眠在这个park成员上, // 其它线程通过这个park唤醒该工作线程 park note // 记录所有工作线程的链表 alllink *m // on allm schedlink muintptr mcache *mcache lockedg *g createstack [32]uintptr // stack that created this thread. freglo [16]uint32 // d[i] lsb and f[i] freghi [16]uint32 // d[i] msb and f[i+16] fflag uint32 // floating point compare flags locked uint32 // tracking for lockosthread // 正在等待锁的下一个m nextwaitm uintptr // next m waiting for lock needextram bool traceback uint8 waitunlockf unsafe.Pointer // todo go func(*g, unsafe.pointer) bool waitlock unsafe.Pointer waittraceev byte waittraceskip int startingtrace bool syscalltick uint32 // 工作线程id thread uintptr // thread handle // these are here because they are too large to be on the stack // of low-level NOSPLIT functions. libcall libcall libcallpc uintptr // for cpu profiler libcallsp uintptr libcallg guintptr syscall libcall // stores syscall parameters on windows mOS }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82P为M执行提供上下文,保存M执行G时的一些资源,包括LRQ、memmory cache等。一个M只有绑定P才能执行G,M被阻塞时P会传递给其他M。// p保存go运行时所必须的资源 type p struct { lock mutex // 在allp中的索引 id int32 status uint32 // one of pidle/prunning/... link puintptr // 每次调用schedule时会加一 schedtick uint32 // 每次系统调用时加一 syscalltick uint32 // 用于sysmon线程记录被监控p的系统调用时间和运行时间 sysmontick sysmontick // last tick observed by sysmon // 指向绑定的 m,如果 p 是 idle 的话,那这个指针是 nil m muintptr // back-link to associated m (nil if idle) mcache *mcache racectx uintptr deferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go) deferpoolbuf [5][32]*_defer // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen. goidcache uint64 goidcacheend uint64 // Queue of runnable goroutines. Accessed without lock. // 本地可运行的队列,不用通过锁即可访问 runqhead uint32 // 队列头 runqtail uint32 // 队列尾 // 使用数组实现的循环队列 runq [256]guintptr // runnext非空时,代表的是一个runnable状态的G,这个G被当前G修改为ready状态,相比runq中的G有更高的优先级。 // 如果当前G还有剩余的可用时间,那么就应该运行这个G,运行之后,该G会继承当前G的剩余时间 runnext guintptr // Available G's (status == Gdead) // 空闲的g gfree *g gfreecnt int32 sudogcache []*sudog sudogbuf [128]*sudog tracebuf traceBufPtr traceSwept, traceReclaimed uintptr palloc persistentAlloc // per-P to avoid mutex // Per-P GC state gcAssistTime int64 // Nanoseconds in assistAlloc gcBgMarkWorker guintptr gcMarkWorkerMode gcMarkWorkerMode runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point pad [sys.CacheLineSize]byte }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58GPM相互作用,G运行在M上,M依赖P提供的资源,P持有待运行的G。
M会从绑定的P上获取可执行的G,也会从network poller获取可运行的G,甚至会从其他P窃取可执行的G。G的状态流转涉及到以下几种情况。
P的状态流转涉及到四种状态,包括程序初始化时的_Pgcstop状态,P初始化时的_Pidle状态,M运行时调用runtime.acquireq促使p变成Prunning并通过runtime.releasep释放,G运行时进入系统调用设置为_Psyscall,如果被系统监控runtime.retake抢夺会重更新变成_Pidle状态。当程序发生GC时,P会被设置为_Pgcstop,GC结束执行runtime.startTheWorld时被重新设置为Prunning。
M只有自旋和非自旋两种状态,自旋状态下会不停找到可做的事情,包括GC、处理G、窃取G,非自旋状态则会休眠,直至被其他工作线程唤醒。
# 2.5.协程退出
协程调用`exit`退出后,会执行清理工作: 1.将G的状态从`_Grunning`变为`_Gdead` 2.清空G的一些字段 3.调用`dropg`函数解除G和M的关系 4.把G放入P的freeg队列缓存,供下次创建G时快速获取,避免重新分配,freeg就是G的对象池 5.调用scheduler函数再次调度1
2
3
4
5
6

# 3.垃圾回收
# 3.1.GC机制
GC垃圾回收是一种自动管理内存方式,程序能够检测和清除不再被使用的内存块,避免内存泄漏。Go程序在内存上分为堆区、栈区、全局数据区、代码段和数据区五个部分,栈上的内存由编译器负责管理回收,堆上内存由编译器和垃圾收集器负责管理回收。
GC可以调用函数手动触发,也可以根据内存分配情况自动触发。当自动触发时,会根据申请内存时的当前已分配内存是否为上次GC内存2倍,或监控线程sysmon检测自上次GC是否超出一定时间,进行相应的GC清理工作。GO中的垃圾清理采用三色标记清除算法,具备以下特点:--- 特点 1.GC和程序同时运行,不会完全阻塞程序执行,标记阶段主要部分并发,通过写屏障确认标记正确性,清理阶段默认并发 2.基于三色标记清除算法收集待回收对象,是一种高效的垃圾回收技术 3.非分代回收,优先回收短声明周期对象,针对长期存在的对象优化性能 4.支持GOGC环境变量调整垃圾回收器的灵敏度 5.采用混合写屏障策略,标记阶段与程序并发执行,保证标记正确性与一致性 --- 三色标记清除原理 1.初始状态,所有对象标记为白色,根对象(全局变量、栈上变量等)标记为灰色 2.遍历灰色对象,将其引用对象标记为灰色直至不可达,已处理的灰色对象标记为黑色 3.清理仍为白色的对象——白色对象不可达1
2
3
4
5
6
7
8
9
10
11GC虽然可以简化内存分配,减少内存泄漏,提高内存利用率,但仍然需要占用资源,存在一定性能开销,可能会对延迟敏感的程序产生影响。因此,应该尽量避免短声明周期对象的创建,避免频繁GC,同时少用使用,避免变量逃逸到堆上。
# 3.2.GC流程
STW阶段,该阶段会设置gcwaiting=1休眠所有M,确保垃圾回收在标记阶段能够准确扫描所有活跃对象,阻止新的对象分配干扰标记过程。
Mark阶段,采用三色标记法并发跟踪对象引用状态,找出程序中存活对象。该过程中,标记任务会分成若干段,分配给gcproc个M,M会检查自身的helpgc是否为为true,检查通过才会执行标记任务。当然,该过程中M仍然涉及到任务窃取,加快标记速度,标记完成后所有参与GC的M再次进入休眠状态。其中,Mark阶段应用程序可能继续执行,导致对象引用关系发生变化,因此这里会加上写屏障记录这些动态变更,标记结束时通过STW确保这些变更被应用,处理引用关系的动态变化。这里的STW很短,一般在数十微妙到几毫秒。
Sweep清理阶段主要释放未标记的内存,回收未被标记的对象,这个阶段可以选择串行或并发,串行模式清理任务会与主GC线程绑定,可能导致较长的STW,并发则通过单独G执行清理,不需要阻塞业务逻辑。
- 最后是
STTW阶段,该阶段会设置gcwaiting=0,通知所有的M线程业务逻辑恢复正常,保证垃圾回收完成后程序能够立即恢复。Go采用非分代回收策略,不区分对象的声明周期,对所有对象一视同仁地标记和回收,强调高效、简洁、低延迟地实现。虽然非分代回收可能在短生命周期对象地处理上效率不如分代回收,但Go本身就有高并发优势,同时避免分代回收内存管理的额外数据结构和复杂性,尽量达到低延迟和实时性。
# 3.3.内存屏障
屏障技术可以理解为一种回调机制,在程序的某种执行过程中加一个判断机制,满足判断机制则执行
回调函数,类似于钩子函数。对于内存操作,可以简单划分为:栈对象的读、栈对象的写、堆对象的读、堆对象的写。实际上,垃圾回收机制只用于回收堆上内存,栈中的内存会随函数调用结束自动释放,也就是屏障技术只能作用于堆内存。屏障机制分为插入写屏障和删除写屏障。插入写屏障实现了强三色不变性,给对象添加引用关系时触发;删除写屏障实现了弱三色不变性,删除对象引用关系时触发。
每当一个对象被引用,就会触发判断:如果这次操作是白色对象被黑色对象引用,就把这个白色对象标记为灰色。由于栈没有屏障机制,可能会出现栈上的对象扫描完成后引用堆上的白色对象,白色对象在堆中没有其他对象引用,这次的引用添加不会吧白色对象置灰,造成堆上的活跃对象被错误回收。

Go语言的处理方法是,GC迭代结束时对栈执行一次标记清除法,该过程会STW,重新扫描一遍栈对象,清除掉从栈对象出发访问不到的对象。删除写屏障类似,每当一个对象被删除时,就会判断:如果是一个灰色对象引用的白色对象被删除,那么把白色对象标记为灰色,保证可能的活跃对象不会被错误回收。
删除写屏障场景下,由于栈没有屏障机制,栈对象删除白色堆对象引用时,堆对象不会标记为灰色,可能会被错误回收。针对这一问题,会在起始时
STW扫描整个栈,把栈上引用的所有对象置灰。插入写屏障和删除写屏障都存在短板,所以Go在1.8版本后结合两者实现混合写屏障,满足变形的弱三色不变性。混合写屏障的操作包括:开始时扫描栈上所有可达对象标记为黑色、扫描期间栈上新建对象标记为黑色、被删除的对象标记为灰色、被添加的对象标记为灰色。
栈对象不会触发屏障机制,但
GC开始时将所有可达对象置黑就保证栈上活跃对象不会被错误回收。此外,对象C属于栈上新建对象,也会置黑。所以新增引用关系时,虽然无法触发屏障机制但活跃对象安全;删除引用关系时,由于B已经只会也不会被错误回收。
当对象被堆对象引用时,混合写屏障会将新增对象标记为灰色,添加引用或删除引用关系对象置为灰色,避免活跃对象被错误回收。

注意
写屏障虽然可以保证引用关系完整性,但会增加内存写操作开销,尤其是高频引用修改情况下。另外,写屏障的实现需要保证低延迟,增加了处理动态引用的复杂性。