一、三色标记法
作为一门现代化的语言,golang与java一样,都在语言中内置了垃圾回收的功能,不需要程序员自己去回收堆内存。而垃圾回收中,最重要的两个部分就是垃圾检测算法以及垃圾回收算法。垃圾检测算法决定哪些对象是垃圾需要被回收,主要有引用计数法和三色标记法。垃圾回收算法决定如何回收内存,主要有标记清除,标记复制,标记压缩等。由于,引用计数法有循环引用的问题,故大部分的语言都是使用三色标记法来检测垃圾的。
三色标记法需要从一些对象出发进行分析,这些对象是必然不能被回收的,如栈对象(栈是程序自动回收的,不归垃圾回收管理),全局变量等,它们会被记录起来,放到一个列表中,这个列表在java中就被称为GC ROOTS,golang也有类似的定义。三色标记法从GC ROOTS出发,通过层层引用,GC ROOTS可以间接引用到的对象(对象可达),就不是垃圾,而GC ROOTS无法间接引用到的对象(对象不可达),就是我们需要回收的垃圾,而使用算法分析对象可不可达的过程,也被称为可达性分析。
三色标记法首先会将GC ROOTS中的对象全部标记为黑色,然后将GC ROOTS引用的对象标记为灰色,加入灰色对象队列。然后不断的扫描灰色对象队列中的对象,将灰色对象引用的对象全部标记为灰色并加入灰色对象队列。然后每扫描完成一个灰色对象,就将该对象标记为黑色,从灰色对象队列中删除,一直重复此过程,直到灰色对象队列中的对象都被扫描完毕。此时,再次扫描整个内存,剩下的白色对象就是需要处理的垃圾。
二、并发垃圾回收
在垃圾回收的过程中,有一部分操作是必须要停止所有的用户线程,这被称为STW(stop the world)。STW时间的长短,是衡量一个垃圾回收算法好坏的一个重要因素。在Golang早期的时候,Golang的垃圾回收是串行的,所以STW时间特别长,达到几百毫秒,在后续的更新中,Golang垃圾回收进行了多次优化。Golang1.8后,STW停顿时间低于1ms。
Golang垃圾回收一般分为2个阶段,标记和清除。而在Golang早期的时候, 标记和清除都要STW,并且标记和清除都是单线程执行。
首先,标记需要扫描整个内存的对象,这也就意味着,内存越大,标记的时间越久,而STW的时间也会越久。其次,单线程只能使用单个cpu,无法最大化的使用多核服务器上的cpu资源。所以在Golang后面的优化中,就改用了多线程来清理垃圾。同时,清理阶段垃圾回收线程可以和用户线程一起并行执行,使STW时间降低了一些。
但是,标记阶段仍然会耗时几百毫秒,对于正常的程序来说,仍然是较难接受的。故必须要对标记进行优化,减少标记阶段的STW时间。golang的思路时,通过STW进行初始标记,然后退出STW状态,通过多个goroutine进行并发标记,标记完之后进入STW,进行再次标记以校准对象的状态,标记完成后,进行清理阶段的准备。最后退出STW状态,恢复用户goroutine,并启动多个垃圾清理协程进行垃圾清理。
之所以需要再次STW进行最终标记,是因为并发标记时,如果用户协程和标记协程对同一个变量进行操作,会产生浮动垃圾(是需要处理的垃圾,但是标记算法误认为它不是垃圾)或者错误标记把有用的对象标记为垃圾。前者只是少回收一点垃圾,但是后者会导致用户的数据丢失,导致程序运行出错。