1.什么是复制算法
GC 复制算法是利用 From 空间进行分配的。当 From 空间被完全占满时,GC 会将活动
对象全部复制到 To 空间。当复制完成后,该算法会把 From 空间和 To 空间互换,GC 也就结
束了。From 空间和 To 空间大小必须一致。这是为了保证能把 From 空间中的所有活动对象
都收纳到 To 空间里。
复制算法执行过程图解
2.复制算法的优点
优秀的吞吐量
GC 标记 - 清除算法消耗的吞吐量是搜索活动对象(标记阶段)所花费的时间和搜索整体堆(清除阶段)所花费的时间之和。
另一方面,因为 GC 复制算法只搜索并复制活动对象,所以跟一般的 GC 标记 - 清除算法相比,它能在较短时间内完成 GC。也就是说,其吞吐量优秀。
尤其是堆越大,差距越明显。GC 标记 - 清除算法在清除阶段所花费的时间会不断增加,
但 GC 复制算法就不会产生这种消耗。毕竟它消耗的时间是与活动对象的数量成比例的。
可实现高速分配
GC 复制算法不使用空闲链表。这是因为分块是一个连续的内存空间。因此,调查这个
分块的大小,只要这个分块大小不小于所申请的大小,那么移动 $free 指针就可以进行分配了。
不会发生碎片化
活动对象被集中安排在 From 空间的开头对吧。像这
样把对象重新集中,放在堆的一端的行为就叫作压缩。在 GC 复制算法中,每次运行 GC 时
都会执行压缩。
因此 GC 复制算法有个非常优秀的特点,就是不会发生碎片化。也就是说,可以安排分块允许范围内大小的对象。
与缓存兼容
那就是 mutator 执行速度极快。近来很多 CPU 都通过缓存来高速读取位置较近的对象。这也是借助压缩来完成的,通过压缩来把有引用关系的对象安排在堆中较近的位置。
2.复制算法的缺点
堆使用效率低下
GC 复制算法把堆二等分,通常只能利用其中的一半来安排对象。也就是说,只有一半堆能被使用。相比其他能使用整个堆的 GC 算法而言,可以说这是 GC 复制算法的一个重大的缺陷。
递归调用函数
在这里介绍的算法中,复制某个对象时要递归复制它的子对象。因此在每次进行复制的时候都要调用函数,由此带来的额外负担不容忽视。大家都知道比起这种递归算法,迭代算法更能高速地执行
改进
使用迭代替递归
执行过程图解
首先复制所有从根直接引用的对象,在这里就是复制 B 和 G。
首先复制所有从根直接引用的对象,在这里就是复制 B 和 G。
搜索 BꞋ,然后把被 BꞋ 引用的 A 复制到了 To 空间,同时把 scan 和 free 一致,所以最后只要把 From 空间和 To 空间互换,GC 就结束了。
接下来按 B、G、A、E 的顺序来搜索对象。第一种GC 复制算法采用的是深度优先搜索,而 第二种的复制算法采用的则是广度优先搜索。
广度优先搜索是需要FIFO队列的,需要把对象一边保持在队列中一边进行搜索
实际上 scan 和 free 每次向右移动,队列里就会追加对象,scan 每次向右移动,都会有对象被取出和搜索。这样一来就满足了先入先出队列的条件,即把先追加的对象先取出。像这样把堆兼用作队列,这个算法的一大优点。不用特意为队列留出多余的内存空间就能进行搜索。
改进后优点
而 这个的 GC 复制算法是迭代算法,因此它可以抑制调用函数的额外负担和栈的消耗。特别是拿堆用作队列,省去了用于搜索的内存空间这一点,实在是令人赞叹。
改进后缺点
改进后具有引用关系的对象可能不再相邻,失去了充分利用缓存的优势
再次改进
使用广度优先遍历的得迭代代替了递归 ,但是同时也失去了引用对象相邻的优势,那么我们改如何同时保留两种优势呢?
下面为大家介绍下面我们将为大家介绍 Paul R. Wilson、Michael S. Lam 和 Thomas G. Moher 于 1991 年提出的近似深度优先搜索法。这个方法虽然只是近似深度优先搜索,不过这样一来就能通过深度优先搜索执行 GC 复制算法了。
首先我们先看一下按到第二种GC遍历是每个页面的内存分布情况
首先假设每个对象都是两个字节,每个页面大小为6个字节
第二种GC页面分布状况
在第 0 页中,A 和其引用的 B、C 已经配置好了,形成了理想的状态。不过,在其他的第 1 页至第 4 页中,同一页面里的对象间都没有引用关系。因此每次访问这些对象时,都要浪费时间去从内存上读取包含这些对象的页面。像这样,虽然程序能在广度优先搜索的一开始把对象安排在理想的状态,但随着搜索的推进,对象的安排就逐渐向着不理想的状态发展了。
在近似深度优先搜索法中有四个重要的变量
• page[i] 指向第 i个页面的开头。
• local_scan[i] 指向第i 个页面中下一个应该搜索的位置。
• free :指向分块开头的指针
执行过程
首先复制 A 到 To 空间,然后搜索 A,复制 B 和 C。它们都被复制到了第 0 页。
因为 A 已经搜索完了,所以 free 在此指向第 1 页的开头,也就是说,在下一复制中对象会被安排到新的页面。在这种情况下,程序会从 local_scan 开始搜索。此外,当对象被复制到新页面时,程序会根据这个页面的 major_scan 还指向第 0 页,所以还跟之前一样从 $local_scan[0] 开始搜索。也就是说,下面要搜索 B
首先复制了被 B 引用的 D,在这里 D 被安排到了 local_scan 进行搜索。此时 local_scan[1] 开始搜索对象 D。通过对 D 的搜索,复制了 H 和 I。
在这里第 1 页已经满了, local_scan[1] 的搜索暂停,
程序开始通过 $local_scan[0] 进行搜索。也就是说,再次开始对 B 的搜索。
对 B 的搜索结束后,E 被复制到了第 2 页。因为程序还要往新页面上复制对象,所以
local_scan[2] 进行搜索。
因此,下一个要搜索的是 E。通过对 E 的搜索复制 J 和 K
通过对 J 和 K 的搜索,第 2 页被填满了, major_
scan,再次通过 $local_scan[0] 进行搜索。
接下来的操作和上述步骤一样,这里就不再详细说明了。搜索完对象 C,复制完 A 到 O
的所有对象之后的状态如图 所示。
这样终于搜索完第 0 页了, page[1]。虽然还有没搜索过的对象,
但这些对象都没有子对象,所以程序不对它们进行复制。
最终的执行结果 如图所示
很明显能够看出,跟 Cheney 的使用广度优先搜索的 GC 复制算法不同,在使用近似深
度优先搜索的情况下,不管在哪一个页面里,对象间都存在着引用关系。
这样就可以既使用迭代代替递归 又可以使用缓存机制了