C语言中的垃圾回收分析
2016-03-05沈俊慧
沈俊慧
摘要:C语言没有运行时库,无法自动压缩使用中的内存,缩小堆栈所需内存空间。若只申请内存,没有释放,势必造成系统内存不断减少、丢失。长时间的运行,最终导致系统死机。文章阐述了C语言垃圾产生的原因,并从引用计数、标记一清除算法两方面提出如何实现C语言的垃圾回收。
关键词:C语言;垃圾回收;引用计数;标记一清除算法
一般来说,操作系统记录了所有进程使用的全部资源,当系统进程结束时,操作系统会将其所有资源回收,包括内存、寄存器和CPU等所使用的资源。对于许多大型系统来说,一个进程或者程序往往会运行很长时间,如网站进程、守护进程等。操作系统不会主动释放这种进程所占用的资源。如果进程在使用完后内存资源却没有及时释放,就会造成系统内存不断减少、丢失,累积到一定程度,会导致系统无内存可用,进而导致系统无法运行或者错误,严重的甚至死机。
C语言是一种可以直接与操作系统底层交互的语言,它没有运行时库。运行时库可以压缩使用中的内存,缩小堆栈所需内存空间。因此C语言编程中的内存管理依赖程序编写人员。因此,本文将详细阐述C语言中的垃圾回收问题,并阐明其产生原因和常用的回收技术等,以帮助基于C语言的程序开发者更好地设计和实现高效的垃圾回收机制。
1 C语言中垃圾产生的原因
在C语言开发中,内存分配有3种方式:一是函数体内的局部变量,在栈上创建。如void fun(){int a;……},变量a在函数fun()执行结束时会自动释放。二是静态存储区域分配。如static int x=1;变量x为静态变量,生存期较长,从第1次使用就始终存在,直到整个程序结束时自动释放。三是动态内存分配,在堆上分配,通过malloc或calloc申请,通过free释放。此种方式比前2种在内存使用上更加灵活,是编写大型程序不可缺少的内存分配方式。但是它却存在一个致命的风险。在一个函数体内通过malloc或calloc申请的动态内存由另外一个函数体使用,程序员很容易忘记在使用后通过free释放,该内存块将保持在系统内并一直处于被分配状态,导致后面程序可申请的内存空间严重不足。并且这种情况下,也很难查找出内存泄露发生在什么地方。这些内存块被称为垃圾。因此在C语言程序中,引入了垃圾收集器概念,它是一种动态存储分配器,可定期识别垃圾块,并相应地调用free函数,将这些垃圾块回收到空闲的内存链表中。
2 C语言中垃圾回收基本原理
垃圾回收因高级编程语言迅猛发展而得到广大程序员的重视,但实际上垃圾回收的历史最早始于1960年的Lisp语言。Lisp语言高度依赖动态内存分配,所有数据几乎都是在堆上分配的。程序设计者必须找到一种方法来自动管理每一块内存,否则程序员会被数不清的“申请内存”和“释放内存”淹没,或者计算机内存很快被消耗殆尽,这促使垃圾回收技术的产生。
一个垃圾回收模块有3个基本要求:无内存泄漏;能够自动回收无用内存;能够内存整理或内存紧缩。
程序中所有已分配的内存都有其对应的指针指向它,若一块内存没有任何指针指向它,称为内存泄漏,即该块内存是无用内存块且不可达。在程序运行的任意状态中,寄存器(考虑多核CPU在多线程下)、程序栈和静态数据段中所有指针的集合叫根集。可达指的是这块内存从根集出发,可以找到一个指向它的指针,不可达就是找不到指向它的指针。垃圾回收器首先从根集出发,沿着指针指向和传递的方向进行扫描,当找到了地址可用的内存时给它做一个标记。然后扫描整个链表内存,并给其作标记。当扫描结束后,对现有内存块进行检查,如果存在没有被标记的内存块,则说明其不在被引用链表中,则将其回收。
3 C语言中垃圾回收的常用算法及实现
目前,内存垃圾回收机制有多种处理机制,如引用计数、标记清除算法、复制算法、标记整理算法、增量收集算法、分代收集算法等。无论何种机制,垃圾回收技术所解决的只有2个重要问题:第一,如何识别当前内存中未被引用的内存;第二,如何将失去管理的内存进行回收。下面本文将从引用计数和标记清除算法2种机制出发阐述C语言如何实现垃圾回收。
3.1 引用计数
引用计数的原理非常简单:对于一个内存块,除内存块本身外增加一个引用计数,每当这个内存块被外部的指针或内存块所引用的时候,引用计数加1;当引用它的指针释放了对它的引用的时候,则引用计数减1,同时检查引用计数的值,当引用计数为0时,就销毁该内存块并释放其所占用的内存。引用计数机制需要2个函数,一个是增加引用计数,一个是减少引用计数并当计数减少到0时释放内存。实现代码如下:
引用计数算法有两大优点:第一是其垃圾回收过程无需打断进程运行,内存管理的开销分布于整个应用程序运行期间,应用程序在正常运行状态下即可完成内存垃圾回收;第二是在于空间上的引用局部性比较好,当某个内存块的引用计数值变为0时,系统就可以立刻回,收内存块而无需二次遍历,相比其他的算法,引用计数简单快捷。引用计数机制很简单,而且是很有效的资源管理方法,在资源充足的条件下,可以在很多场景中都得到有效的应用。
但是,引用计数也存在一些缺点:首先是环形引用。比如现在内存块X引用内存块Y,Y内存块的计数器加1,然后Y内存块引用Z内存块,Z的计数加1,后来z又引用Y,Y的计数加1得到2。假如现在X不再引用Y了,Y的计数器成为1。而由于Y,Z互相引用,形成一个环回导致内存块Y,Z永远无法被回收。即无法释放作为循环数据结构的一部分的结构。其次,每次在内存块创建或者释放时,都要计算引用计数值,增加系统运行时间。由于每个内存块要保持自己被引用的数量,必须分配一定的变量空间来存放计数器,增加额外的内存。第三,引用计数占用了结构中的第1个位置,而大部分机器中最快可以访问到的就是这个位置。
3.2 标记一清除算法
标记清除(Mark Sweep)算法是Lisp的语言设计者提出并应用于Lisp语言的算法。该算法分成2个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器扫描每个内存块,对被引用的内存块进行标记。在标记完成后,统一检测内存集中所有内存,将未被标记的内存块进行回收。
标记清除算法的核心问题是如何标记所有已经被引用的内存块。当内存分配时,可能超过某个阀值,然后触发垃圾回收。首先垃圾回收器建立一些根集合在静态数据段、寄存器和栈中。然后垃圾回收器会从根集出发查找所有的内存块引用,然后在被引用的内存块上作标记(mark),当垃圾回收器遇上一个已经被标记的对象后就不再扫描了,以防止造成环回。这个阶段就是所谓的标记阶段(Mark Phase)。当标记阶段结束后,所有被标记的内存块就称为可达对象(Reachable Object)或活对象(Live Object),而所有没有被标记的内存块则被认为是垃圾,可以被回收。这个阶段进行完就进入了清除阶段(Sweep Phase)。垃圾回收器检测内存集,逐一释放未被标记的内存。整个过程分为2个阶段:找到所有存活内存块的标记阶段;清除所有未标记的内存块的清除阶段。
标记阶段实现代码如下所示:
相比较引用计数算法,标记清除算法可以避免环回引用问题,同时也减少了在操作引用计数上带来的时间开销。但是标记清除算法是一种“停止启动”算法,在垃圾回收器运行过程中,应用程序必须暂时停止。此外,标记清除算法在标记阶段需要遍历所有的存活内存块,会造成一定的时间开销。在清除阶段,清除垃圾对象后会造成大量的内存碎片。因此标记清除算法的主要研究问题在于如何减少程序停顿时间和对内存碎片的整理。
4 结语
本文详细阐述了C语言垃圾产生的原理,分析了垃圾回收的基本原理,在此原理上详细阐述实现垃圾回收的2种算法的优缺点以及实现的代码。希望通过详细分析2种算法的机制,帮助开发人员应对实际问题,开发出高效的C程序。