GC基本算法及C++GC机制

前言

垃圾收集器是一种动态存储分配器,它自动释放程序再也不须要的已分配的块,这些块也称为垃圾。在程序员看来,垃圾就是再也不被引用的对象。自动回收垃圾的过程则称为垃圾收集(garbage collection)。在一个支持垃圾收集的语言中,程序显式地申请内存,但从不须要显式的释放它们。垃圾收集器会按期识别垃圾块,并将垃圾块放回空闲链表中。显然,C语言的malloc包不是一个带GC功能的分配器,程序员显式 调用malloc分配内存,也须要显式调用free释放它。而像java、C#这些语言等则提供了垃圾收集器。这篇文章的内容为介绍一些经常使用的GC算法,同时简单提一下C++的GC机制。html

基本概念

有向可达图与根集

垃圾收集器将存储器视为一张有向可达图。图中的节点能够分为两组:一组称为根节点,对应于不在堆中的位置,这些位置能够是寄存器、栈中的变量,或者是虚拟存储器中读写数据区域的全局变量;另一组称为堆节点,对应于堆中一个分配块,以下图:
java

当存在一个根节点可到达某个堆节点时,咱们称该堆节点是可达的,反之称为不可达。不可达堆节点为垃圾。可见垃圾收集的目标便是从从根集出发,寻找未被引用的堆节点,并将其释放。程序员

三种基本的垃圾收集算法及其改进算法

垃圾收集算法是一个重要而活跃的研究领域,自从20世纪60年代开始对垃圾收集进行研究以来,垃圾算法的研究从未中止。常见的垃圾收集算法有一下这几种类型:算法

一、引用计数算法

引用技术算法是惟一一种不用用到根集概念的GC算法。其基本思路是为每一个对象加一个计数器,计数器记录的是全部指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,若是指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。这个思路能够参考C++ 引用计数技术及智能指针的简单实现并发

引用计数算法的优势是实现简单,在原生不支持GC的语言中也能容易实现出来。另外一个优势这种垃圾收集机制是即时回收,也便是对象再也不被引用的瞬间就当即被释放掉。而其缺点是若存在对象的循环引用,没法释放这些对象,例图:
函数

缺点二是多个线程同时对引用计数进行增减时,引用计数的值可能会产生不一致的问题,必须使用并发控制机制解决这一问题,也是一个不小的开销。.net

二、 Mark & Sweep 算法

这个算法也称为标记清除算法,为McCarthy首创。它也是目前公认的最有效的GC方案。Mark&Sweep垃圾收集器由标记阶段和回收阶段组成,标记阶段标记出根节点全部可达的对节点,清除阶段释放每一个未被标记的已分配块。典型地,块头部中空闲的低位中的一位用来表示这个块是否已经被标记了。经过Mark&Sweep算法动态申请内存时,先按需分配内存,当内存不足以分配时,从寄存器或者程序栈上的引用出发,遍历上述的有向可达图并做标记(标记阶段),而后再遍历一次内存空间,把全部没有标记的对象释放(清除阶段)。所以在收集垃圾时须要中断正常程序,在程序涉及内存大、对象多的时候中断过程可能有点长。固然,收集器也能够做为一个独立线程不断地定时更新可达图和回收垃圾。该算法不像引用计数可对内存进行即时回收,可是它解决了引用计数的循环引用问题,所以有的语言把引用计数算法搭配Mark & Sweep 算法构成GC机制。
线程

三、 节点复制算法

Mark & Sweep算法的缺点是在分配大量对象时,且对象大都须要回收时,回收中断过程可能消耗很大。而节点复制算法则恰好相反,当须要回收的对象越多时,它的开销很小,而当大部分对象都不须要回收时,其开销反而很大。
算法的基本思路是这样的:从根节点开始,被引用的对象都会被复制到一个新的存储区域中,而剩下的对象则是再也不被引用的,即为垃圾,留在原来的存储区域。释放内存时,直接把原来的存储区域释放掉,继续维护新的存储区域便可。过程如图:指针

能够看到,当被引用对象(非垃圾对象)不少时,须要复制不少的对象到新存储区域。htm

分代回收

以上三种基本算法各有各的优缺点,也各自有许多改进的方案。经过对这三种方式的融合,出现了一些更加高级的方式。而高级GC技术中最重要的一种为分代回收。它的基本思路是这样的:程序中存在大量的这样的对象,它们被分配出来以后很快就会被释放,但若是一个对象分配后至关长的一段时间内都没有被回收,那么极有可能它的生命周期很长,尝试收集它是无用功。为了让GC变得更高效,咱们应该对刚诞生不久的对象进行重点扫描,这样就能够回收大部分的垃圾。为了达到这个目的,咱们须要依据对象的”年龄“进行分代,刚刚生成不久的对象划分为新生代,而存在时间长的对象划分为老生代,根据实现方式的不一样,能够划分为多个代。

一种回收的实现策略能够是:首先从根开始进行一次常规扫描,扫描过程当中若是遇到老生代对象则不进行递归扫描,这样可大大减小扫描次数。这个过程可以使用标记清除算法或者复制收集算法。而后,把扫描后残留下来的对象划分到老生代,如果采用标记清除算法,则应该在对象上设置某个标志位标志其年龄;如果采用复制收集,则只须要把新的存储区域内对象设置为老生代就能够了。而实际的实现上,分代回收算法的方案五花八门,经常会融合几种基本算法。

而其余的改进算法数量很是庞大,但大都基于上述的三种基本算法。

C++垃圾回收机制

C语言自己没有提供GC机制,而C++ 0x则提供了基于引用计数算法的智能指针进行内存管理。也有一些不做为C++标准的垃圾回收库,如著名的Boehm库。借助其余的算法也能够实现C/C++的GC机制,如前面所说的标记清除算法。

当应用程序使用malloc试图从堆上得到内存块时,一般都是以常规方式来调用malloc,而当malloc找不到合适空闲块的时候,它就会去调用垃圾收集器,以回收垃圾到空闲链表。此时,垃圾收集器将识别出垃圾块,并经过free函数将它们返回给堆。这样看来,垃圾收集器代替咱们调用了free函数,从而让咱们显式分配,而无须显式释放。

上图中的垃圾收集器为一个保守的垃圾收集器。保守的定义是:每一个可达的块都可以正确地被标记为可达,而一些不可达块却可能被错误地标记为可达。其根本缘由在于C/C++语言不会用任何类型信息来标记存储器的位置,即对于一个整数类型来讲,语言自己没有一种显式的方法来判断它是一个整数仍是一个指针。所以,若是某个整数值所表明的地址刚好的某个不可达块中某个字的地址,那么这个不可达块就会被标记为可达。因此,C/C++所实现的垃圾收集器都不是精确的,存在着回收不干净的现象。而像JAVA的垃圾收集器则是精确回收。在《关于C++ 0x 里垃圾收集器的讲座》这篇文章里提到,C++标准提案中使用gc_strict、 gc_relax这样的关键字来描述一个内存区内有没有指针,但没法精确到每一个数据上。实际上,早在07年,一份C++标准提案N2670就提出要将垃圾回收机制做为加入C++,最后提案是没有经过,其缘由大概是由于实现复杂,因为语言自己缘由存在这样那样的限制。因此在C++ 0x中除了shard_ptr、weak_ptr这些智能指针外,咱们并没看看到GC机制的身影。而至于C++是如何解决引用计数的循环引用问题以及并发控制问题,咱们将以另一篇文章进行介绍。

(完)

参考书籍

深刻理解计算机系统 [美]Randal E.Bryant / David O'Hallaron 机械工业出版社

代码的将来 [日] 松本行弘 人民邮电出版社

C++标准提案N2670 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2670.htm
关于C++ 0x 里垃圾收集器的讲座 http://blog.csdn.net/g9yuayon/article/details/1702694

相关文章
相关标签/搜索