GoLang之GC系列一(SWT)

1.垃圾

从进程虚拟虚拟空间来看,程序的要执行的指令在代码段,全局变量、静态数据都会分配在数据段,而函数的局部变量、参数、返回值都可以在函数的栈帧中找到。但是由于函数的额栈帧会在函数返回后销毁,如果不在编译阶段确定数对象的大小,或者声明周期会超出当前所在函数,那就不适合分配在栈上,而应该分配在堆上,在栈上使用其在堆上的地址。

随着数据的运行有些数据便不会再被用到了,直接分配在栈上的数据会随着函数调用栈的销毁释放自身占用的内存;
但是这些在堆上分配的数据就不一样了,他们占用的内存需要程序员主动释放才可以重新使用,否则就会成为垃圾,越急积越积的垃圾会不断地消耗系统内存;

2.手动回收垃圾

有些编程语言需要程序员在编写程序时手动释放那些不再需要的、分配在堆上的数据(C、C++),这被成为手动垃圾会回收,但是一旦释放早了,后续对该数据的访问就会出错,因为被释放的内存已经可能被清空或重新分配,甚至已经还给操作系统了,这就是所谓的的“悬挂指针的问题了”,但是如果一直占用内存就会出现内存泄露,所以越来越多的编程语言支持“垃圾自动回收”;

3.自动垃圾回收

即由运行时识别不再有用的数据并释放他们占用的内存,并释放他们占用的内存,内存何时被释放、释放的内存何时处理,这写问题都不用我们操心,的确很方便,那么自动垃圾回收怎么区分哪些数据对象是垃圾呢?

还得从虚拟空间来看,可以确定的是程序中用得到的数据一定时从栈、数据段这些根节点追踪得到的数据,即也就是说以这些地方直接追踪到的变量作为根节点,可以追踪到的数据范围大于等于真正的有用数据。
虽然能够追踪的得到的不代表后续一定会用到,但是从这些根节点追踪不到的数据就一定不会被用到,也就一定是垃圾,所以目前主流的垃圾回收算法都是使用“可达性”近似等价于“存活性“的;

4."标记–清扫"算法

顾名思义,先把垃圾都标记出来,然后再把垃圾都清除掉;

要识别出存活对象可以把栈、数据段的数据对象作为root,基于这些进一步追踪,把能追踪的数据都进行标记,剩下的追踪不到的就是垃圾了 ,这就是标记–清扫算法的核心思想

"三色抽象"可以清晰的展现追踪式垃圾对象状态的变化过程:
1.垃圾回收开始时所有数据都是白色的

2.然后把直接追踪的root节点都标记为灰色,灰色代表基于当前节点展开的追踪还未完成

3.当基于某个节点的追踪的任务完成后,便会把该接点标记为黑色,表示它是存活数据,而且无需基于它再次进行追踪了

4.当没有灰色节点时,就意味着标记工作可以结束了,此时有用数据都为黑色,垃圾都为白色,接下来回收这些白色对象的内存即可

标记–清扫算法实现起来相对比较简单,但是比较容易造成内存碎片化,而面对大量不连续的小分块内存,要找到合适的内存分块的代价更高,也会造成很多小块内存无法使用的情况,碎片化会影响内存分配与程序执行的效率。

这一问题可以基于BIBOP(Big Bag Of Pages)的思想,把内存划分为多种大小规格,对相同规格的内存块进行统一管理,这样可以更快的匹配到大小合适的空闲内存,规格类型划分的更合理,也有助于内存使用率;

这一问题,可以配合相应的内存管理模型来缓解。例如Tcmalloc内存管理模型这样,把内存块分成不同的规格进行统一管理,可以很好的应对碎片化问题。还有人提出了标记——压缩(整理)算法。

5."标记–压缩(整理)"算法

标记——压缩算法的标记阶段与标记——清扫算法相同,不同的是,它会在完成标记工作后对堆内存的使用进行压缩。所谓的压缩,就是移动非垃圾数据,使它们尽可能紧凑的放在内存中;

虽然标记——压缩算法有效解决了内存碎片化的问题,但是带来的多次扫描与移动开销也不容小觑;
标记——压缩算法比较鲜明的特点便是它会移动数据来减少碎片化,还有一种复制式回收算法,也会移动数据。

6."复制回收"算法

(1)复制式回收算法会把堆内存划分成两个相等的空间,From和To。程序执行时使用From空间;

(2)垃圾回收执行时会扫描From空间,把能追踪到的数据复制到To空间。
(3)当所有有用的数据都复制到To空间后,把From和To空间的角色交换一下。原来的To空间用作From,原来的From空间则可以全部回收作为新的To空间

每一轮垃圾回收都是如此,这种复制式回收也不会带来碎片化问题,而且因着使用连续的内存块,可以实现高速的内存分配。但是明显的不足之处就是只有一半的堆内存可以被使用。
为了提高堆内存的使用率,通常会和其它垃圾回收算法搭配使用,只在一部分堆内存中使用复制式回收。例如在分代回收中就经常搭配使用复制式回收。


7.“分代回收”算法

分代回收的提出,主要是基于弱分代假说(weak generational hypothesis):

大部分对象都在年轻时死亡

如果我们把新创建的对象称为“新生代对象”,把经受住特定次数的垃圾回收而依然存活的对象称为“老年代对象”。
基于弱分代假说,新生代对象成为垃圾的概率高于老年代对象,所以可以把数据划分为新生代和老年代,降低老年代执行垃圾回收的频率。
对于标记、复制式等追踪类回收算法而言,不用每次都扫描所有数据,将明显提升垃圾回收执行的效率,而且新生代和老年代还可以分别采用不同的回收策略,进一步提升回收效益并减少开销。

分代回收算法大多通过复制式回收来处理新生代对象,只有经历过一定次数的垃圾回收还能依然存活的新生代对象才会被晋升为老年代对象。

虽然分代回收算法将回收的注意力主要集中在新生代对象上,但是考虑到老年代到新生代的引用,也依然做不到只扫描新生代就把回收工作完成的地步。
到目前为止我们介绍的多为追踪式回收,都需要在执行垃圾回收时扫描数据识别垃圾对象,而引用计数式垃圾回收有所不同。

8."引用计数"算法

引用计数指的是一个数据对象被引用的次数,程序执行过程中会更新对象及其子对象的引用计数。当引用计数更新到0时,就表示这个对象不再有用,可以回收它占用的内存了。
所以,引用计数法不用专门执行扫描任务,因为垃圾识别的任务已经分摊到每一次对数据对象的操作中了。
这样说起来简单,但实现起来却并不容易。虽然引用计数法可以及时回收无用内存,但是高频率的更新引用计数也会造成不小的开销。而且还要专门想办法识别循环引用的情况,因为循环引用会导致引用计数无法更新到0,造成对应的内存无法被回收的情况(即一下情况:若是A引用了B。B也引用了A,形成了循环引用,当A和B的引用计数更新到只剩彼此的相互引用时用计数无法更新到0,造成对应的内存无法被回收的情况)

9.总结

所以没有哪种算法是万能的,只能在其核心思想之上不断完善优化,而且不只要完善算法本身存在的问题,还要考虑到实际应用中不可回避的需求

我们上一次的介绍都是在暂停用户程序,只专注于进行垃圾回收的前提下展开的,也就是所谓的STW(Stop The World)。
这首当其冲的问题就是:

用户程序可以接受长时间的暂停吗?

实际上,我们总是希望能够尽量缩短STW的时间,所以又出现了“增量式垃圾回收”,请看下节