垃圾回收 复制算法

GC复制算法


4 GC复制算法

  Copying GC是Marvin L.Minsky在1963年研究出来的算法。就是指把某个空间里的活动对象复制到其它空间,把原空间里的所有对象都回收掉。在此,将复制活动对象的原空间称为From空间,将粘贴活动对象的新空间称为To空间。

4.1 什么是GC复制算法

  GC复制算法是利用From空间进行分配的。当From空间被完全占满时,GC会将活动对象全部复制到To空间。当复制完成后,该算法会把From空间和To空间互换,GC也就结束了。From空间和To空间大小必须一致。这是为了保证能把From空间中的所有活动对象都收纳到To空间里。 
  GC复制算法

4.1.1 执行过程

  假设目前堆里的配置如下。 
  初始状态
  执行GC。首先是从根直接引用的对象B和G,B先被复制到了To空间。 
B被复制之后
  将B被复制后生成的对象称为B’。在From空间中B已经被打上了复制完成标签。但是,这里只把B’复制了过来,它的子对象A还在From空间里,下面把A复制到To空间里。 
  A被复制之后

  这次才是真正意义上复制了B。因为A没有子对象,所以对A的复制就完成了。 
  接下来,要和复制B一样从根引用复制G,以及其子对象E。虽然B也是G的子对象,不过因为已经复制完B了,所以只要把从G执行B的指针转换到B’上。 
  最后,只要把From空间和To空间互换,GC就结束了。 
GC结束后
  对象C、D、F因为没法从根查找,所以会被回收。这里程序是以B、A、G、E的顺序搜索对象的,使用的是深度优先搜索。

4.2 优点

4.2.1 优秀的吞吐量

  GC标记-清除算法消耗的吞吐量是搜索活动对象(标记阶段)所花费的时间和搜索整体堆(清除阶段)所花费的时间之和。 
  另一方面,因为GC复制算法只搜索并复制活动对象,所以跟一般的GC标记-清除算法相比,它能在短时间内完成GC,也就是说其吞吐量优秀。 
  尤其是堆越大,差距越明显。GC标记-清除算法在清除阶段所花费的时间会不断增加,但GC复制算法就不会。因为它消耗的时间是与活动对象的数量成比例的。

4.2.2 可实现高速分配

  GC复制算法不使用空闲链表,因为分块是一块连续的内存空间。因此,调查这个分块的大小,只要这个分块大小不小于所申请的大小,那么移动指针就可以进行分配了。 
  比起GC标记-清除算法和引用计数算法等使用空闲链表的分配,GC复制算法明显快得多。使用空闲链表是为了找到满足要求的分块,需要遍历空闲链表,最坏的情况是我们不得不从空闲链表中取出最后一个分块,这样就用了大量时间把所有分块都调查一遍。

4.2.3 不会发生碎片化

  基于算法性质,活动对象被集中安排在From空间的开头。像这样把对象重新集中,放在堆中一端的行为叫作压缩。在GC复制算法中,每次运行GC时都会执行压缩。 
  因此GC算法有个非常优秀的特点,就是不会发生碎片化,也就是说可以安排分块允许范围内大小的对象。 
  另一方面,在GC标记-清除算法等GC算法中,一旦安排了对象,原则上就不能再移动它了,所以会多多少少产生碎片化。

4.2.4 与缓存兼容

  在GC复制算法中有引用关系的对象会被安排在堆里离彼此较近的位置。B’引用A’,G’引用E’的顺序排列。这种情况有一个优点,那就是mutator执行速度极快。很多CPU都通过缓存来来高速读取位置较近的对象。这也是借助压缩来完成的,通过压缩来把有引用关系的对象安排在堆中较近的位置。

4.3 缺点

4.3.1 堆使用率低下

  GC复制算法把堆分成二等分,通常只能利用其中一半来安排对象。也就是说只有一半堆能被使用,相比其他能使用整个堆的GC算法而言,这是GC复制算法的一个重大缺陷。

4.3.2 不兼容保守式GC算法

  GC标记-清除算法有着跟保守式GC算法相兼容的优点。因为GC标记-清除算法不用移动对象。 
  另一方面,GC复制算法必须移动对象重写指针,所以有着跟保守式GC算法不相容的性质。虽然有限制条件,GC复制算法和保守式GC算法可以进行组合。

4.3.3 递归调用函数

  在算法中,复制某个对象时要递归复制它的子对象,因此在每次进行复制的时候都要调用函数,由此带来的额外负担不容忽视。比起递归算法,迭代算法更能有效地执行。 
  此外,因为在每次递归调用时都会消耗栈,所以还有栈溢出的可能。

4.4 Cheney的GC复制算法

  C.J.Cheney于1970年研究出了GC算法。Cheney的GC算法不是递归地,而是迭代地进行复制。

4.4.1 执行过程

  与GC复制算法不同,这里引入了scan指针。 
初始状态
  在Cheney复制算法中,首先复制从根直接引用的对象,在这里就是复制B和G。 
复制BG之后
  在这时scan仍然指着To空间的开头,free从To空间的开头向右移动了B和G个长度。关键是scan每次对复制完成的对象进行搜索时,以及free每次对没复制的对象进行复制时,都会向右移动。剩下的就是重复搜索对象和复制,直到scan和free一致。下面是对B’的搜索。 
搜索B'之后
  搜素B’,然后把B’引用的A复制到了To空间,同时把scan和free分别向右移动。下面是检索的G’。搜索G’后,E被复制到了To空间,从G’指向B的指针被换到了B’。 
  下面该搜索A’和E’了,不过它们没有子对象,所以即使搜索了也不能进行复制。因为在E’搜索完成时,scan和free一致,所以最后只要把From空间和To空间互换,GC就结束了。 
GC结束之后
  GC复制算法采用的是深度优先搜索,而Cheney的复制算法采用的则是广度优先搜索。

4.4.2 被隐藏的队列

  广度优先搜索需要先入先出结构的队列,即把该搜索的对象保存在队列中,一边取出一边进行搜索。 
  实际上,scan和free之间的堆变成了队列,scan左边是已经搜索完毕的对象空间。也就是说,free每次向右移动,队列里就会追加对象,scan每次向右移动,都会有对象被取出和搜索。这样就满足了先入先出队列的条件,即先追加的对象先取出。 
  像这样把堆用作队列,正式Cheney算法的一大优点,不用特意为队列留出多余的内存空间就能进行搜索。

4.4.3 优点

  GC复制算法是递归算法,而Cheney的GC复制算法是迭代算法,因此它可以抑止调用函数的额外负担和栈的消耗。特别是拿堆用作队列,省去了用手搜索的内存空间。

4.4.4 缺点

  GC复制算法中,具有引用关系的对象是相邻的,因此才能充分利用缓存的便利。Cheney的复制算法中有引用关系的对象并不相邻。Cheney的GC复制算法兼容缓存,只能说它比GC标记-清除算法和引用技术算法好一些而已。

4.5 近似深度优先搜索方法

4.5.1 Cheney的GC复制算法

  对象引用关系
GC复制算法中各个对象的配置
  各页面的容量只有6个字,也就是说只能放下3个对象。A和被A引用的B、C是相邻摆放的。这就形成了访问局部性的理想状态。 
  其他的对象距离有引用关系的对象较远,这样一来,就降低了本来很有可能被连续读取的对象同时位于缓存中的可能性,降低了缓存命中率。

4.5.2 近似深度优先搜索方法执行过程

 1. page:将堆分割成一个个页面的数组。page[i]指向第i个页面的开头。
 2. local_scan:将每个页面中搜索用的指针作为元素的数组。local_scan[i]指向第i个页面中下一个应该搜索的位置。
 3. major_scan:指向搜索尚未完成的页面开头的指针。
 4. free:指向分块开头的指针。

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

  首先复制A到To空间,然后搜索A,复制B和C。他们都被复制到了第0页。 
复制并搜索A,复制B和C之后 
  因为A已经搜索完了,所以local_scan[0]指向B。free在此指向第1页的开头,也就是说,在下一次复制中对象会被安排到新的页面。在这种情况下,程序会从major_scan引用的页面的local_scan开始搜索。 
  此外,当对象被复制到新页面时,程序会根据这个页面的local_scan进行搜索。搜索会一直持续到新页面被对象全部占满为止。 
  此时因为major_scan还指向第0页,所以还跟之前一样从local_scan[0]开始搜索。也就是说下面搜索B。 
搜索B复制D之后 
  首先复制了被B引用的D,在这里被安排到了page[1]的开头,像这样被安排到页面开头时,程序会使用该页面的local_scan进行搜索。此时,local_scan[0]的搜索暂停,程序根据local_scan[1]开始搜索对象D。通过对D的搜索,复制了H和I。 
搜索D,复制H和I之后
  在这里第1页已经满了,free指着第2页的开头。因此local_scan[1]的搜索暂停,程序开始通过local_scan[0]进行搜索。也就是说,再次开始对B进行搜索。 
搜索B,复制E之后 
  对B搜索结束后,E被复制到了第2页。因为程序还要往新页面上复制对象,所以local_scan[0]的搜索再次暂停,开始通过local_scan[2]进行搜索。因此,下一个要搜索的是E。通过对E的搜索复制J和K。 
搜索E,复制J和K之后
  通过对J和K的搜索,第2页被填满了,free指向第3页的开头。因此回到major_scan,再次通过local_scan[0]进行搜索。搜索完对象C,复制A到O的所有对象之后的状态图。 
复制完A到O所有对象之后
  这样终于搜索完第0页了,major_scan指向page[1]。虽然还有没搜索过的对象,这些对象都没有子对象,所以程序不会对它们进行复制。

4.5.3 执行结果

通过近似深度优先搜索安排对象
  和Cheney的使用广度优先搜索的GC复制算法不同,在使用近似深度优先搜索的情况下,不管在哪一个页面里,对象间都存在着引用关系。 
  这是因为此算法采用的不是完整的广度优先搜索,而是在每个页面上分别进行广度优先搜索。这里利用了广度优先搜索的性质,即在搜索一开始把有引用关系的对象安排在同一个页面中。

4.6 多空间复制算法

  GC复制算法最大的缺点是只能利用半个堆。这是因为该算法将整个堆分成了两半,每次都腾出一半。 
  那么把堆再作细分呢?我们不把堆分成2份,而是分成10份,其中需要拿出2块空间分别作为From空间和To空间来执行GC复制算法。反正无论如何都要空出1块空间来当To空间,那我们就把这个额外负担降到整体的1/10就可以了。 
  接下来,必须使用别的方法对剩下的8块空间执行GC,在这里使用GC标记-清除算法。 
  多空间复制算法说白了就是把堆N等分,对其中2块空间执行GC复制算法,对剩下的N-2块空间执行GC标记-清除算法,也就是把这2种算法组合起来使用。

4.6.1 执行过程

  将N设置为4。 
开始执行第一次GC前
  To空间空着,其他的3个空间都安排有对象,在这个状态下执行GC就会变成下图这样。 
第1次GC结束后
  将heap[0] 作为To空间,将heap[1]作为From空间执行了GC复制算法。此外,在heap[2]和heap[3]中执行了GC标记-清除算法,将分块连接到了空闲链表,当mutator申请分块时,程序会从这个空闲链表或heap[0]中分割出分块给mutator。 
  接下来,将To空间和From空间分别移动一个位置,将heap[1]作为To空间,将heap[2]作为From空间,执行下面的GC。 
  mutator基于这个状态重新开始执行。 
在开始执行第2次GC之前
  这次heap[1]是To空间,heap[2]是From空间。 
第2次GC结束后
  heap[2]的活动对象都被复制到了heap[1]中,在heap[0]和heap[3]中执行了GC标记-清除算法。 
  此外,为了准备下一次GC,将heap[2]设为To空间,将heap[3]设为From空间。

4.6.2 优点

  多空间复制算法没有将堆二等分,而是分割成了更多块空间,从而更有效地利用了堆。以往的GC复制算法那只能利用半个堆,而多空间复制算法仅仅需要空出一个分块,不能使用的只有1/N个堆。

4.6.3 缺点

  执行GC复制算法的只有N等分的凉快空间,对于剩下的N-2块空间执行的是GC标记-清除算法。因此就出现了GC标记-清除算法固有的问题,分配耗费时间、分块碎片化等。 
  只有把执行GC标记-清除算法的空间缩小,就可以缓解这些问题。例如,N=3,就能把碎片化的空间控制在整体堆的1/3。不过这时候为了在剩下的2/3的空间里执行GC复制算法,就不能使用其中的一半,也就是堆空间的1/3。 
  综上,不管是GC标记-清除算法还是GC复制算法,都有各自的缺点。几乎不存在没有缺点的万能算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值