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

时间:2021-03-21 00:01:58

前言

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

基本概念

有向可达图与根集

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

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

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

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

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

1、引用计数算法

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

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

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

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

2、 Mark & Sweep 算法

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

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

3、 节点复制算法

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

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

可以看到,当被引用对象(非垃圾对象)很多时,需要复制很多的对象到新存储区域。

分代回收

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

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

而其他的改进算法数量非常庞大,但大都基于上述的三种基本算法。

C++垃圾回收机制

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

GC基本算法及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++是如何解决引用计数的循环引用问题以及并发控制问题,我们将以另外一篇文章进行介绍。

(完)

参考书籍

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

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

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

代码的未来 [日] * 人民邮电出版社

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