【002】垃圾回收算法之-GC标记-清除算法

世界上首个值得纪念的GC算法那是GC标记-清除算法(Mark Sweeep GC)

什么是GC标记-清除算法

就如它的字面意思一样,GC标记-清除算法由标记阶段和清除阶段构成。标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把哪些没有标记的对象,也就是非活动对象回收的阶段。通过这两个阶段,就可以令不能利用的内存空间重新得到利用。

目录

什么是GC标记-清除算法


标记阶段

 在标记阶段中,collector会为堆里的所有活动对象打上标记。为此,我们首先要标记通过根直接引用的对对象。首先我们标记这样的对象,然后递归的标记通过指针数组能访问到的对象。这样就能把所有的活动对象都标记上了。

注意:在引用中包含了循环等的情况下,即使对已被标记的对象,有时程序员也会可能再次标记,我们得注意避免重复标记。

如果标记未完成,则程序会再对象的头部进行置位操作。这个位要分配在对象的头之中,并且能用obj.mark访问。意思是若obj.mark为真,则表示对象已标记;若obj.mark为假,则对象没有被标记。

标记完所有活动对象后,标记阶段就结束了。标记阶段结束时的堆如图所示:

在标记阶段中,程序会标记所有活动对象。毫无疑问,标记锁花费的实际是与”活动对象的总数“成正比的。

以上是关于标记阶段的说明。用一句话概括,标记阶段就是”遍历对象并标记“的处理过程。这个”遍历对象“的处理过程在GC中是一个非常重要的概念。 

重点

深度优先搜索与广度优先搜索

具体的搜索算法不做详解,GC会搜索所有对象。不管使用什么搜索方法,搜索相关的步骤(调查的对象数量)都不会有差别。

另一方面,比较一下内存使用量(已存储的对象数量)就可以知道,深度优先搜索比广度优先搜索更能压低内存使用量。因此我们在标记阶段经常使用深度优先搜索。

清除阶段

在清除阶段中,collector会遍历整个堆,回收没有打上标记的对象(垃圾)使其占用内存空间能再次得到利用。

设置了标志位,就说明这个对象是活动对象。活动对象比如是不能回收的。等到此次回收完毕,我们就取消已经标记的活动对象的状态标记位,准备下一下GC。

我们必须把非活动对象回收再利用。回收对象就是把对象作为分块,连接到被称为”空闲链表“的单向链表。在之后进行分配时只要遍历这个空闲链表,就可以分找到分块了。

在清除阶段,程序会遍历所有堆,进行垃圾回收。也就是说,所花费时间与堆的大小成正比。堆越大,清除阶段所花费的实际就会越长。

注意事项:

1.分配

 这里的分配是指将回收的垃圾进行再次利用。那么,分配是怎样进行的呢?也就是说,当mutator申请分块时,怎样才能把大小合适的分块分配给mutator?

我们在清除阶段以及把垃圾对象链接到空闲的链表了。搜索空闲链表并寻找大小合适的分块,这项操作就叫分配。

First-fit 、Best-fit、Worst-fit的 差别

分配策略大致分为以上三种,

First-fit: 遇到第一个大于等于它需要的内存块的时候就返回。

Best-fit :遇到最合适的大小的时候就返回

Worst-fit:找出空闲链表中最大的分块,将其分割成mutator申请的大小和分割后剩余的大小,目的是将分割后剩余的分块最大化。但是因为Worst-fit很容易产生大量小的分块,所以不推荐大家使用此方法。

出去Worst-fit,剩下的还有Best-fit和Fist-fit这两种。我们使用单纯的空闲链表时,考虑到分配所需要的实际,选择使用Fisst-fit更为明智。

2.合并

根据分配策略的不同可能会产生大量的小的分块。但如果他们是连续的,我们就能把所有小的分块链接在一起形成一个大分块。这种”链接连续分块“的操作就叫做合并(coalescing),合并是在清除阶段进行的。

优点

 1.实现简单

  说到GC 标记-清除 算法的优点,那当然要数算法简单,实现容易了。打个比方,在引用计数法中就很难切实管理计数器的增减,实现困难。

另外,如果算法实现简单,那么它与其他算法的组合也就相应的简单。

2.与保守式GC算法兼容

保守式的算法中,对象是不能被移动的。因此保守式GC算法根把对象从现在的场所复制到其他场所的GC复制算法与标记-压缩算法不兼容。

而GC标记-清除算法因为不会移动对象,所以分厂适合搭配保守式GC算法那。事实上,在很多采用保守式GC算法的处理程序中也用到了GC标记-清除算法。

缺点

1.碎片化

 在GC标记-清除算法的使用过程中会逐渐产生被细化的分块,不久后就会导致无数的小分块散步在堆的各处。我们称这种情况为碎片化)(fragmentation).众所周知 Windows的文件系统也会产生这种现象

如果发生碎片化,那么即使堆中分块的大小总大小够用,也会因为一个个的分块都太小而不能执行分配。

如果发生碎片化,就会增加mutator的执行负担。把具有引用关系的对象安排在堆中较远的位置,就会增加访问所需要的时间。

因为分块在堆中的分布情况取决于mutator的运行情况,所以只要使用GC标记-清除算法,就会或多或少地产生碎片化。

3.分配速度

 GC标记-清除算法中分块不是连续的,因此每次分配都必须遍历空闲链表,找到足够大的分块。最糟糕的情况就是每次分配都得把空闲链表遍历到最后。

3.与写时复制技术不兼容

写时复制技术(copy-on-write)是在LInux等众多UNIX操作系统的虚拟存储中用到的高速化方法。打个比方,在Linux中复制进程,也就是使用fork()函数时,大部分内存空间都不会被复制。只是复制进程,就复制了所有内存空间的话也太说不过去了吧。因此在写是复制技术只是装作已经复制了内存空间,实际上是将内存空间共享了。

在各个进程中访问数据时,能够访问共享内存就没有什么问题。

然后,当我们堆共享内存空间进行写入时,不能直接重写共享内存。因为从其他程序访问时,就会发生数据不一致的情况。在重写时,要复制自己私有空间的数据,对这个私有空间进行重写。复制后值访问这个私有空间,不访问共享内存。像这样,因为这门技术是”在写入时候进行复制“的,所以才被称为写时复制技术。

这样的话,GC标记-清除算法那就会存在一个问题-与些时复制技术不兼容。即使没有重写对象,GC也会设置所有活动对象的标志位,这样就会频繁发生本不应该发生的复制,压迫到内存空间。

4多个空闲链表

标记-清楚算法中只用到一个空闲链表,在这个空闲链表中,对大的分块和小的分块进行同样的处理。这样,每次分配的时候都要遍历一次空闲链表来寻找合适 大小的分块,这样非常浪费时间。

为了优化这种情况,就是利用分块大小不同的空闲链表,即创建只能连接大分块的空闲链表和只能连接小分块的空闲链表。这样一来,只要按照mutator所申请的分块大小选择空闲链表,就能在短时间内找到符合条件的分块了。

有得必有失,这样速度快了,但是牺牲了空间。