垃圾回收器一直是被诟病最多,也是整个运行中改进最努力的部分。所有变化都是为了缩短STW时间,提高程序实时性。
大事记:
上述并发是指垃圾回收和用户逻辑并发执行。
按照官方的说法,Golang GC的基本特征是“非分代、非紧缩、写屏障、并发标记清理”。
The GC runs concurrently with mutator threads, is type accurate (aka precise), allows multiple
GC thread to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is
non-generational and non-compacting. Allocation is done using size segregated per P allocation
areas to minimize fragmentation while eliminating locks in the common case.
The algorithm decomposes into several steps.
与之前的版本在STW状态下完成标记不同,并发标记和用户代码同时执行让一切都处于不稳定状态。用户代码随时可能修改已经被扫描过的区域,在标记过程中还会不断分配新对象,这让垃圾回收变的很麻烦。
究竟什么时候启动垃圾回收?过早会严重浪费CPU资源,影响用户代码执行性能。而太晚,会导致堆内存恶性膨胀。如何正确平衡这些问题就是个巨大的挑战。
所有问题的核心:抑制堆增长,充分利用CPU资源。为此,引入一系列举措:
这是让标记和用户代码并发的基本保障,基本原理:
当完成全部扫描和标记工作后 ,剩余不是白色就是黑色,分别代表要待回收和活跃对象,清理操作只需要将白色对象内存收回即可。
控制器全程参与并发回收任务,记录相关状态数据,动态调整运行策略,影响并发标记单元的工作模式和数量,平衡CPU资源占用。当回收结束时,参与next_gc回收阈值设置,调整垃圾回收触发频率。
gcController implements the GC pacing controller that determines when to trigger concurrent
garbage collection and how much marking work to do in mutator assists and background marking.
It uses a feedback control algorithm to adjust the memstats.next_gc trigger based on the heap
growth and GC CPU utilization each cycle.
This algorithm optimizes for heap growth to match GOGC and for CPU utilization between assist
and background marking to be 25% of GOMAXPROCS.
The high-level design of this algorithm is documented at https://golang.org/s/go15gcpacing.
某些时候,对象分配速度可能远快于后台标记。这会引发一系列恶果,比如堆恶性扩张,甚至让垃圾回收永远无法完成。
此时,让用户代码线程参与后台回收标记就非常有必要。在为对象分配堆内存时,通过相关策略去执行一定限度的回收操作,平衡分配和回收操作,让进程处于良性状态。
引用计数的思想非常简单:每个单元维护一个域,保存其他单元指向它的引用数量。当引用数量为0时,将其回收。引用计数是渐进式的,能够将内存管理的开销分布到整个程序之中。C++的share-ptr使用的就是引用计数方法。
引用计数算法实现一般是把所有单元放在一个单元池里,比如类似free list。这样所有的单元就被串起来了,就可以进行引用计数了。新分配的单元计数值被设置为1(注意不是0,因为申请一般都是ptr=new(struct)这种)。每次有一个指针被设为指向该单元时,该单元的计数值加1;而每次删除某个指向它的指针时,它的计数值减1.当其引用计数为0的时候,该单元会被进行回收。虽然这里说的比较简单,实现的时候还是有很多细节需要考虑,比如删除某个单元的时候,那么它指向的所有单元都需要对引用计数减1.那么如果这个时候,发现其中某个指向的单元引用计数又为0,那么是递归的进行还是采用其他的则略呢?递归处理的话会导致系统颠簸。等等细节。。。。
优点:
缺点:
标记-清扫算法是第一种自动内存管理,基于追踪的垃圾手机算法。算法思想在70年代就提出了,是一种非常古老的算法。内存单元并不会在编程垃圾立即回收,而是保持不可达状态,直到达到某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是STW,转而执行垃圾回收程序。垃圾回收程序对所有的存活单元进行一次全局遍历确定那些单元可以回收。算法分两个部分:标记(mark)和清扫(sweep)。标记阶段表明所有的存活单元,清扫阶段将垃圾单元回收。可视化可以参考下图。
标记-清扫算法的优点也就是基于追踪的垃圾回收算法具有的优点,避免了引用计数算法的缺点(不能处理循环引用,需要维护指针)。缺点也很明显STW
节点复制也是基于追踪的算法。其将整个堆等分为两个半区,一个包含现有数据,另一个包含已被废弃的数据。节点复制式垃圾收集从切换两个半区的角色开始,然后收集器在老的半区,也就是Fromspace中遍历存活的数据结构,在第一次访问某个单元时把它复制到新半区,也就是Tospace中去。在Fromspace中所有存活单元都被访问过之后,收集器在Tospace中建立一个存活数据结构副本,用户程序可以重新开始运行了。
优点:
缺点:
基于追踪的垃圾回收算法(标记-清扫、节点复制)一个主要问题是在声明周期较长的对象上浪费时间(长生命周期的对象是不需要频繁扫描的)。同时,内存分配存在一个事实“most object die young”。基于这亮点,分代垃圾回收算法将对象按生命周期长短存放到堆上的两个(或者更多)区域,这些区域就是分代。对于新生代的区域的垃圾回收频率要明显高于老年代区域。
分配对象的时候从新声代里面分配,如果后面发现对象的生命周期较长,则将其移到老年代,这个过程叫做promote。随着不断的promote,最后新生代的大小在整个堆的占用比例不会特别大。收集的时候几种主要经历在新生代就会相对来说效率更高,STW时间也会更短。
优点:
缺点:
参考:Go 1.5源码剖析
参考:http://legendtkl.com/2017/04/28/golang-gc/