GC基本算法分析

国庆期间零零散散看了一遍《垃圾收集》这本书,全书原理性的东西居多,涉及到很多本科计算机组成原理方面的知识,很值得一读。但是这本书出版 的时间比较长了,那时候的计算机系统和现代的处理能力及侧重方面有一定的差别,书本上的计算机语言Lisp我们也可能有些陌生,但是基本的原理是 不变的,所以关注垃圾收集就值得一读。

我们组正在研究docker,就不得不接触GO语言,其是我见过第一个在不内置虚拟机的情况下使用垃圾收集的语言,说明即使是静态语言使用垃圾收集也 是一种趋势,它让程序员无需关心内存泄漏等棘手的问题。现在的GO只是使用很简单的垃圾收集方式(停机-标记-清扫),我相信随着Go语言的发展,他 的GC机制也会越来越完善,当然也会越来越复杂,所以了解一定的GC原理是有必要的。

引用计数算法

算法描述:

  • 每个单元(对象)都有一个额外的域,存放引用计数值,内存管理器必须为每个单元维护引用计数值,使之等于指向该单元的指针的数量,这些指针 来自其他堆单元或者根。当然,这一算法将所有单元放在一个自由单元池中,这个池的实现常常是一个链表,每个单元有一个指针域,即为next域 ,所有单元通过它链接成一条长链。

  • 自由单元的引用计数的值为0,当一个新单元被分配时其引用计数被设置为1,当一个引用其的单元被释放时, 其引用计数的值减1,当然如果当一个拥有其指针的新单元生成时,其应用技术加1。所以在分配或者释放单元 时其指向的单元的引用计数值需要被即时更新,如果一个单元的引用计数值降为0的时候,它自动被回收。

  • 引用计数算法在分配新单元的时候,需要将其引用的所有单元的引用计数值加1,这时的复杂度取决于其引用的 单元的数量,通常这不是问题的关键所在。在更新一个单元的时候,其引用单元的引用计数值可能降为0,这时候需 要回收这个引用单元,这可能触发一系列的回收行为,所以需要递归地回收废弃的单元内存,处理环形引用的情况 更为复杂

优势和缺点:

  • 引用计数的长处是将内存管理的开销分布在整个计算过程中, 对存活单元和废弃单元的处理行为与用户程序的 执行交织在一起。与其他基于追踪的GC算法不同的是其不需要停机回收,所以具有更“平滑”的响应时间。

  • 引用计数其GC过程是与用户程序的执行是同步的,其回收过程不会遍历整个单元图,所以其具有更好的程序局部 性,即其对于虚拟存储器是友好的。

  • 大多数内存单元(对象)一般都是新鲜建立就被收回的,所以引用计数能即时地清理这些短命的对象(类似栈分配) ,这也是引用计数的一大优势。

  • 引用计数最大的缺点就是需要维护引用不变式,每改写一个指针都需要修改旧单元和新单元的引用计数,这往往 会带来高昂的代价,而且引用计数与用户程序高度耦合,而且递归过程中不能有任何疏忽,这也给程序的可维护性 带来脆落性。

  • 此外引用计数算法还需要每个单元分配额外的空间来存储引用计数,最具有说服力的缺点是它不能处理环形引用 结构。

标记-清扫算法

算法描述:

  • 标记清扫算法是一种基于追踪的垃圾收集技术,在这一方案下,内存单元并不会在编程垃圾的同时立刻回收,而是 保持不可达和未被发现的状态知道所有可用的内存都耗尽。如果此时再次出现对新单元的请求,系统会暂时挂起“有用” 的程序,并调用垃圾收集例程,将堆中所有当前并未使用的单元清扫回自由单元池中。

  • 标记清扫算法依靠对所有存活单元进行一次全局遍历来确定哪些单元可以回收。这个遍历从根出发,标明所有可以到 达的单元,这些就是存活单元。除此之外所有其他节点都是垃圾,可以送回自由单元池中去,如果垃圾收集器成功地回收 了足够的内存,用户程序的请求就能得到满足,并且可以继续进行工作,如果不能,就需要对堆的大小进行调整,否则程 序将因内存不足而崩溃。

优势和缺点:

  • 与引用计数相比,标记-清扫算法拥有两个优点,它可以非常自然地处理环形结构,不需要采取特别的预防措施;再者 它操纵指针没有任何开销。但是它是基于追踪的垃圾收集技术技术,所以在垃圾回收的时候,用户程序必须暂停,所以可能 对于实时系统、高交互系统和分布式系统并不适用,给出一个简单的解决方案就是在关键的时间段内禁止垃圾收集。

  • 简单的标记-清扫算法还会倾向于使内存空间更破碎,让单元散步在整个堆中,这即为课本中的关键概念-内存碎片,所以 内存碎片造成的分配内存更加困难、失去空间局部性带来的颠簸现象(如果同一结构需要分配多个单元,由于内存连续性已被 破坏,所以两个时间上相邻分配的对象会在空间上相距甚远,造成空间局部性被破坏)等等也会发生在标记-清扫算法中。

  • 如果堆中的内存占有率比较高,即每次回收带来的自由空间有限,这样也会导致垃圾收集变得越来越频繁,这些都将导致 系统性能随着堆空间占有率的提高而下降。

节点复制算法

算法描述:

  • 节点复制收集器将整个堆分为两个半区,一个包含现有的数据,另一个包含已被废弃的数据,节点复制式垃圾收集从切换两 个分区的角色开始。然后收集器在老的半区,也就是Fromspace中遍历存活的数据结构,在第一次访问某个单元时把它复制到新的 半区,也就是Tospace中去。在Fromspace中所有存活单元都被访问过之后,收集器在Tospace中建立了一个存活数据结构的复本, 用户程序可以重新开始运行了。

  • 节点复制垃圾收集天生就有一个有益的副作用:所有存活的数据结构都缩并地排列在Tospace的底部,与那些存在内存碎片问 题的收集器相比,缩并的手机器能够更高效地分配对象。New过程只需要检查有没有足够的空间即可,然后再递增指向自由空间开 始处的指针free。与标记-清扫技术一样,节点复制技术不会给像更新指针这样的用户程序操作带来额外的负担。

优势和缺点:

  • 节点复制技术具有很明显的优势就是极大地降低内存分配的开销,检查空间是否耗尽只需要做简单的指针比较,内存碎片问题 不复存在,而且它也有很好的程序空间局部性。

  • 然而节点复制算法最直接的代价就是使用了两个半区,这样一来,它所需要的地址空间是非复制式收集器的两倍。但是在虚拟 存储系统下有些人觉得不以为然,觉得虚拟存储器提供了足够的程序运行空间,并将不活动的半区换出到辅助存储器上,但是这种 观点忽略了换页的开销。除非两个半区能同时存放在物理内存中,否则由于节点复制式收集器所使用的页的数量是标记-清扫式收集 器的两倍,也将会遇到更多的缺页错误。

  • 上面一点中,如果工作集过大,节点复制算法的缺页率会升高,而其带来的好处引用局部性相比于缺页来说不值得一提。

  • 节点复制算法也会随着堆中内存占用率的提升而出现性能下降,道理很简单节点复制带来的时间开销和回收收益小导致GC不停地 调用。但是与标记-清扫算法相比孰优孰劣很难说,与客户程序的行为属性有很大关系。

总结

  • 我们最熟悉的GC语言java是使用的分代式垃圾回收器,它是一个将前面几种算法很好地综合的典范,如它将堆内存分作几个区来 分别进行垃圾收集,我们暂且使用两个区(新生代和老年代)来讨论,内存的分配工作发生在新生代中,由于新生代对象的寿命一般比 较短,所以采取节点复制算法能获得很好的效率。在老年带中一般是常住内存的年龄大的对象,而且其大小也可能比较大,所以采用 标记-清扫(或者标记-缩并)来清理会更方便。这样分代式垃圾回收器就有两个核心问题,一个是新生代到老年代的提升阈值,另一个 是老年代很新生代中的指针互指问题如何处理。

  • 从上面的分析我们可以看出垃圾回收器的性能问题一般分为四个方面去考虑:低的CPU开销、低的空间开销、较好的程序局部性 (即较好的虚存和cache性能)、较短的中断时间。总的来说,引用计数算法很少被采用,而被广泛使用的是节点复制算法,或者是其与 标记-清扫相结合的方式。



Previous     Next
zhing /
Published under (CC) BY-NC-SA in categories GC  tagged with GC