一种计算机自动内存管理的系统及方法

文档序号:6555910阅读:287来源:国知局
专利名称:一种计算机自动内存管理的系统及方法
技术领域
本发明涉及计算机内存管理技术,更具体地说,涉及一种尤其适用于面向对象的编程开发系统,实现精确的、确定性回收的、无暂停的、高效的内存垃圾回收的系统和方法。
背景技术
对于计算系统的运行效率和可靠性来说,内存的管理是相当重要的。自动内存管理(Automatic MemoryManagement)技术或者说自动存储回收(Automatic Storage Reclamation)技术将程序员从繁琐的手工内存管理中释放出来,并同时减少了程序员犯错误的可能性,提高了系统运行的可靠性。
没有自动内存管理之前,程序员需要显式地通过free或者delete等类似手段回收那些位于堆(Heap)中的内存块(或者称为对象),忘记回收将会造成内存的浪费并影响程序运行的效率,相反如果错误地回收那些正在使用中的内存,则会带来灾难性的后果,导致程序崩溃。自动内存管理技术通过计算机程序自动找出不再使用的内存块,释放它们,令应用程序可以再次利用这些内存。通常人们将这些不会被应用程序使用的内存块称为垃圾(Garbage);而那些正在使用中或者可能被使用的内存称为活动的(Live);错误指向已经回收了的内存的指针称为悬挂指针(dangling pointer)。
自动内存管理,或者说垃圾回收器(Garbage Collector),有很多的实现方式,主要包括两大类引用计数(Reference Counting)和引用跟踪回收(Reference Tracing Collecting)。
采用引用计数的垃圾回收器,系统为堆上每一个对象都维护一个计数器,当一个对象被创建并且被引用时,这个计数就被置为1。当有新的变量引用该对象,计数器进行自加运算。当一个引用超出作用范围或者被赋予新值的时候,计数器进行自减运算。引用计数为0的对象,会被作为垃圾回收。引用计数的运行时的开销比较大,更致命的是如果垃圾对象之间相互循环引用,则引用计数方法就无法回收这些对象,因此这些年来,引用计数对于严格的垃圾回收器设计者来说吸引力不大,常常排除在狭义的垃圾收集算法之外。但是,作为另一方面,第一,引用计数可以在内存变成垃圾的那一时刻,就立即将内存回收,这一特性称为确定性地回收(Deterministic Reclamation),而这往往是其他回收方法所缺乏的;第二,由于引用计数的维护工作总是交织在应用程序的执行过程中,维护所造成的管理开销的粒度小、时间短,不会长时间地中断应用程序运行,比较适合实时(Real-Time)性要求高的场合。
采用引用跟踪方法的垃圾回收器,总的来说是通过跟踪对象之间的相互引用关系,从根集(Root Set)开始扫描,确定根集中的指针,然后以这些指针为起点,跟踪遍历所有可达的对象,在遍历过程中遇到的对象,就被标记为活动。当遍历完成以后,那些没有被标记的对象,就被作为垃圾回收了。根集的定义是指可以被应用程序直接访问,而无须通过指针间接访问的数据。具体的说,是指应用程序可以直接访问的指针变量的集合(包括局部变量、参数、类变量)。引用跟踪回收方法中,最基本是标记-清扫(Mark andSweep)方法,其他还有标记-合并(Mark-Compact)等变种。相对引用计数回收方法,引用跟踪回收方法的最大优势是它能够正确地回收所有的垃圾对象,包括循环引用的垃圾。随着Java语言的出现和发展,采用引用跟踪方法的垃圾回收器越来越流行,甚至成为垃圾回收GC(Garbage Collection)的同义词。其中一个典型的例子就是微软公司使用引用计数机制的COM体系,正在逐步被抛弃,而转向使用引用跟踪方法的垃圾回收机制的.NET平台。
尽管引用跟踪的垃圾回收机制从首先提出到现在已经有40多年的历史,其间有大量的改进的实现方法申请了专利,但是距离一个理想的垃圾回收器还有相当的距离。几个主要的基本问题始终没有得到彻底的解决,一直困扰着人们,随着GC的越来越广泛的流行,这些问题的解决越发显得迫切。我们可以从微软公司这些年来不断改变GC的接口、引入新的GC概念,略见一斑。这些问题包括垃圾收集导致应用程序的暂停、垃圾收集产生的运行开销、内存的利用率低导致对性能的影响、和不确定地回收的垃圾对象等等。下面,对于本发明所要解决的GC的主要问题作一简单地描述。
(1)非确定的垃圾对象回收该问题是指人们无法确定某个垃圾对象何时被系统回收,从而导致与资源管理RAII(Resource Acquisition Is Initialization)的核心内容相冲突。
引用跟踪的垃圾回收,(以后简称为垃圾回收或GC,它不包括引用计数的方法;使用自动内存管理来泛指含引用计数的垃圾回收方法),系统在进行引用遍历的过程是相当耗费资源的,包括耗费处理器CPU资源和遍历导致的内存缓冲失效的开销,后者尤其重要,而且该操作的复杂程度正比于系统中的活动对象而不是垃圾对象的个数,所以即使已经没有垃圾对象了,执行回收操作仍然带来很大的开销。所以回收操作不能经常执行,造成在两次回收操作之间变成垃圾的对象不能及时回收。分代回收(GenerationCollection)和并发回收(Concurrent Collection)等改进方法提高了垃圾回收的频率,但是仍不能保证对象的及时回收,因为在一次遍历过程中变成垃圾的对象极有可能仍需等待下一次的回收的时候,才能得到真正的回收,详细说明参见本发明实施例中的解释。此外,常规的GC实现例如微软的.NET平台和Sun公司的Java,通常只有在内存紧张的时候才进行一次完整的(非分代的)垃圾回收,如果运行的机器有足够多的内存,则某些垃圾对象在程序结束之前永远不会被回收。
资源管理RAII的理念是C++等主流开发语言所倡导的,其核心内容是把资源托管给对象,用对象代表资源。资源的申请和释放很自然地和对象的构造与析构操作对应起来,在对象的构造过程中申请并获得资源,而在对象的析构过程中释放资源,析构过程在对象被回收时执行。资源的定义很广泛,包括打开的文件、网络连接、软件许可证、图形界面的窗口,…,等,通常可以理解为个数相当有限的、而对该类资源的请求的竞争又相当剧烈的对象。这些资源对象要求在使用完后,尽快地释放。而这一点恰恰是垃圾回收所欠缺的,对象的回收总是不确定的、不及时的,特别是在一些特殊的场合,对象之间不存在循环引用的关系,例如软件模块的动态加载和卸载过程,更加希望系统能够象引用计数那样支持确定的析构过程的执行,包括有次序的执行这些析构函数。
由于系统不支持确定的对象回收和析构,给应用程序的设计和编制带来了很多不便。Java引入弱引用(weak reference)来辅助的终结(finalization)函数的执行,.NET引入Dispose成员函数显式地释放对象所关联的资源,最进又引入Destructor函数和栈内析构的概念,目的都是为了尽量减少GC机制带来的不便。但是,由于垃圾回收核心并没有大的改进,这些提供给应用程序的辅助手段并没有太大作用,程序员被迫显式地、即手工地管理对象的资源释放,容易发生忘记释放或提前释放资源错误,破坏了数据一致性,造成系统的不稳定。在某些有众多资源相互依赖的情况下,手工确定哪些资源需要释放相当困难,而且资源的依赖性有传递的特性,一个深藏的、未被注意的对象可能间接地经过多个其他对象,依赖某个资源,如果错误地提前释放这些资源,将导致将来的程序执行失败。所以由于不支持确定性的析构,妥协的改进只是令事情简单的更简单、复杂的更复杂,将维护对象之间的依赖关系推回给了应用程序。
本发明的一个实施例总是立即释放没有引用的对象,因此资源的释放可以由系统自动完成。
(2)内存的利用率低Java、.NET编制的程序,在运行时占内存比较多,这是一个不争的事实。其原因很多,其中一个就是激发垃圾回收操作的条件,为了避免GC操作带来的巨大开销,系统通常仅在需要的时候,即内存不足的情况下才执行完整的回收操作,而仅仅一个未及时回收的对象就可能引用大量其他对象,占据大量的内存。一个没有被引用的对象在GC环境中并不能立即释放,必须依赖GC遍历操作来检测确定,所以在两次GC操作其间,内存只是分配而不会释放,内存使用量总是不断增加,直到GC操作才恢复到真正的、必须的使用值。也就是说,内存的使用量总是锯尺形状的变化的,而且总是大于等于实际的必要使用量,在GC操作前浪费的内存达到最大值。
分代垃圾回收方法及变种,只是尽快地回收了一部分的垃圾,其回收能力随应用程序的不同变化很大,除非最老一代的垃圾得到回收,才真正释放了不需要的内存。
本发明则随时释放引用数为0的对象,也就是说在没有循环引用的情况下,本系统没有多余的内存占用;而在有循环引用的情况下,多余占用的内存仅仅是这些循环引用的内存,并且在GC回收操作时全部回收,大大提高了内存利用率。
(3)导致应用程序暂停到目前为止,引用跟踪垃圾回收操作总是不可避免地造成或多或少的应用程序暂停。有很多自称无暂停的垃圾回收系统是指用户感觉不到暂停,例如,暂停小于1毫秒的、面向多媒体系统的垃圾回收器。它们基本上都是使用增量(Incremental)回收方法及变种,无法保证或者预测最坏情况下会造成应用系统多长时间的暂停,暂停的长短取决于特定的应用程序,包括的对象的使用情况、垃圾回收时的应用程序的执行情况、回收的速度与引用关系变化的速度等等。这些系统的暂停时间大多是通过实际测试得出的,并没有理论上的最坏情况的保证,所以通常被称为软实时的系统。在后面的实施例描述中,详细分析了现有技术导致暂停的原因,主要是安全点(GC-Safe)及根集扫描等。
暂停导致的一个主要问题是暂停相关线程是异步执行的,而且没有优先级别的概念,而且需要暂停所有的线程以保证取得引用关系的一个数据一致的快照。也就是说,执行垃圾回收操作的线程有可能需要阻塞高优先级的线程而等待低优先级别的线程,所以有暂停的垃圾回收系统很难在要求线程迅速作出响应的系统中应用,例如操作系统内核。
本发明的一个实施例彻底地去除了垃圾回收操作造成应用程序暂停。
(4)C++环境下很难高效、正确地精确回收引用跟踪回收的精确性,主要是指系统能够精确标记活动的对象。这是保证完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。C++环境下,编译器未能提供有效的根集指针分布情况,特别是线程运行栈内的指针分布。因此,比较流行的方法是使用保守的(conservative)回收方式,它将类似指针的数据都当作有效的指针进行引用遍历。缺点是不能将所有的垃圾都全部回收,也不能回收隐藏的指针,例如位域(bit-field)和联合(Union),一个错误保留下来未被回收的对象可能引用大量其他的对象或者引用大块的内存,因此造成大量的内存泄露。而且,某些编译器进行的优化处理可能导致某些指针即使是保守的猜测也不能检测到,产生GC-Unsafe的代码点,如果在这些代码点进行垃圾回收,则有可能造成错误地回收活动的对象,造成灾难性的系统崩溃。其他一些实现方式,例如使用各式各样的数据结构,将根集中的指针变量逻辑上汇聚在一起,例如使用单链表的美国专利US 6,907,437 B1,或者使用指针句柄数组的方法等。这些方法面向的是根集中的指针变量,由于根集的变量访问相当频繁,而且是编译器优化的重点目标,它们的缺点是开销太大,而且有些设计同样存在保守回收的GC-Unsafe问题,所以在业界很少见到相关的应用。参考Hans-J.Boehm的conservative garbagecollection及《A Proposal for Garbage-Collector-Safe C Compilation》一文。
本发明的基础就是无需编译器的特殊支持,实现精确的引用跟踪回收。
(5)引用计数和引用跟踪很难合并在一起很早以前人们就开始考虑能否将引用计数和引用跟踪组合起来、取长补短,但很可惜,都不成功。主要的原因是这两者的开销都很大,而且引用跟踪有很多各式各样的缺点,简单的合并往往造成缺点的叠加,而且两者有可能同时回收同一个对象,如何同步这些操作,将带来新问题,最终因为代价太大,被很多人所否定。Brian Harry就曾就此问题以Resource Management为题在网上展开了有关的讨论,可以作为参考。
本发明的一个实施例成功地将引用计数和引用跟踪结合在一起,并将运行开销降到比常规的引用计数还要小很多。
有关垃圾回收的各种基本方法可以参考Paul R.Wilson的《Uniprocessor Garbage CollectionTechniques》一文。
上述的C++语言仅仅是一种典型代表,其实质是指那些编译器不提供垃圾回收信息的开发环境。

发明内容
针对上述目前的自动内存管理中存在的问题,本发明的目的在于提供一种在公知计算机系统内部运行的自动内存管理的系统和方法。通过本发明解决了目前的垃圾回收机制不能提供确定性回收所引发的一系列问题;彻底地解决了垃圾回收造成应用程序暂停的问题;扩大了垃圾回收机制的使用范围,包括在C++的开发环境下、在不支持线程挂起的环境下、甚至在操作系统内核中。
本发明通过在应用程序运行时动态标识部分受管理的对象,这些对象被来自扩展根集的指针所引用,在系统进行垃圾回收操作的时候,首先确定了这些被标识出来的对象,然后以它们作为跟踪遍历的起点,进行引用跟踪,及其他剩余工作。其主要特点是避开了目前的根集扫描操作,这样所有的应用程序就自然地成为GC-Safe的代码,可以随时被中断,垃圾回收操作可以在任意执行点进行,成为无暂停增量回收的基础之一;不再需要编译器的特殊支持,可以在C++为代表的语言环境中完成精确的引用跟踪回收;动态地标识扩展根集引用的对象,为对象的动态立即回收打下基础,使引用计数和引用跟踪两种回收方法可以有机地结合在一起。
本发明的一个实施例采用引用计数的方式对上述对象进行标识,称为锁定计数,并维护一个代表其他受管理对象的引用个数的引用计数器,当两个引用计数器的值均为0的时候,立即回收该对象。在适当的时候,进行引用跟踪回收操作,回收循环引用的垃圾对象。如果应用程序精心设计,消除了循环引用的数据结构,则垃圾回收操作可以无须进行,这样就提供了一种优化手段给予程序开发者。对于一个受管理的对象,通过区分来自不同区域的、不同功效的指针,采用不同的处理方法,其中对于线程运行栈内的指针,包括函数参数、自动变量、返回值,采用一种特殊的处理方法,避免了引用计数器的维护操作,大大降低了整体的引用计数所产生的开销。该实施例解决了资源管理RAII和垃圾回收之间的矛盾,统一回收对象及资源,用户只需在析构函数中编写资源释放的代码,系统便自动在适当的时候释放相关资源,无须用户手工参与,简化的应用程序的体系设计,提高了应用程序的可靠性和稳定性。
在避开根集扫描的基础上,改进了垃圾回收方法,在引用计数维护代码中加入写屏障(Write Barrier)的相关处理,改进了3色或2色的增量垃圾回收方法,允许应用程序不断修改引用关系的同时,确保跟踪遍历能够及时正确完成,从而彻底地消除了垃圾回收导致应用程序的暂停。发明体现在跟踪遍历的过程抽象成为以下步骤(1)将所有对象从“黑”色切换成“白”色,并初始化其它的相关数据;(2)将被应用程序标识的锁定对象转成“灰”色,并开始跟踪遍历,将“灰”色的对象转成“黑”色;在此其间,应用程序可以不断地改变引用关系,这些改变被立即捕获而不是之后的某个时刻,赋值操作导致相关的对象从“白”变“灰”,并被系统跟踪遍历;新创建的对象则直接变“黑”;删除的对象可以立即删除或者延迟删除;(3)步骤2本身保证了它一定能够结束,一旦发生没有了“灰”色对象,即可结束跟踪遍历的过程。通过这样的方法,消除了目前增量回收方法要求全部线程到达一个数据一致的状态下,才能够判断跟踪遍历的是否可以结束,消除了始终必须暂停所有线程来检查全局的数据的可达性。(说明黑、白、灰三种颜色为增量回收方法中的常用表述,仅逻辑上的代表,不意味需要存在实际上的标志)。
这些发明可以综合使用,也可以单独使用或者配合其他方法使用,特别是增量回收方法的改进,在满足一定的必要条件下,可以轻易地整合到将来的其他的回收方法中。一个整合了上述方法的自动内存管理器有这样的优势可以运行于更加广泛的平台上,包括C++语言环境下,提供精确的引用跟踪回收,支持隐藏的指针,以及对本地对象的跟踪;提供统一的资源管理和对象管理,确定性的对象回收和析构过程调用;提供彻底无暂停的增量垃圾回收,可预测最坏情况,适用于要求实时响应的操作系统内核;提供高利用率的内存管理,任何时候仅仅是循环引用的垃圾对象浪费了内存,精心设计的应用程序可以完全不需要垃圾回收操作;运行时的管理开销远低远于常规的引用计数方法,配合减少了垃圾回收的次数和利用了C++语言的执行效率,系统的整体效率相当令人满意。


通过以下借助附图的详细描述,将会更容易地理解本发明,其中图1是一个根据本发明的并入自动内存管理能力的计算机系统的方框图;图2是图1中自动内存管理系统的实施例一的方框图;图3A是一个受管理对象的结构示意图;图3B是一个受管理对象的示意图;图3C是一个受管理对象的引用关系示意图;图4是一个“Stop-The-World”的引用跟踪的垃圾回收方法的流程图;图5A是赋值操作开始之前的初始状态的示意图;图5B是赋值操作之后的示意图;图5C是赋值操作之后,且原来的引用失效之后的示意图;图6是原来的应用程序伪代码;图7是对图6进行修改的部分;图8是修改之后的应用程序伪代码;图9A是返回对象指针的操作发生之前的初始状态示意图;图9B是返回对象指针的操作发生之后的状态示意图;图10A是一个增量垃圾回收的流程图;图10B是图10A中确定锁定对象的详细流程图。
图11是自动内存管理系统实施例二的方框图;图12A是增量回收的标记阶段的流程图;图12B是增量回收的对象引用关系的示意图;图13是实施例二的增量跟踪回收器的标记阶段的内部数据方框图;图14是实施例二的增量回收操作的标记阶段的流程图;图15是实施例二的赋值操作的流程图;图16是实施例二的创建新对象的流程图;具体实施方式
术语“对象”用在本文中是指一个C++的自定义结构,或者一个动态分配的内存块;术语“引用”用在本文中是指一个对象的地址被另一个变量,特别是指针变量所使用,通过该地址,可以访问该对象的成员数据和调用该对象的成员函数。
术语“指针”和“引用”在大多数情况下可以互换,不过有的时候“指针”多用于占一定内存空间的变量,而“引用”多仅仅是对象的地址的数值。或者说指针通常指C语言中的“左值”,而引用通常指C语言的“右值”。
术语“左值”和“右值”与C/C++的定义相同,来源于赋值操作等号左右的表达式的类型。左值通常有确定的地址,而右值是一个数值。
术语“白”、“黑”和“灰”,在本文中按照引用跟踪垃圾回收的惯例,分别代表不可达对象或尚未遍历的对象、已经完成了遍历的活动对象、及尚未执行遍历操作的活动对象。
术语“集合”、“常规集合技术”和“集合管理手段”,在本文中是指在逻辑上实现集合的基本操作的数据结构,包括各种链表(List)、数组(Array)、向量(Vector)、聚集(Collection)、集合(Set)、图(map)、哈希表(Hash Table)等,及其变种和组合。通常不必自己另外编写,可以直接使用C++标准库,例如STL。
术语“引用计数”及“引用数”,在本文中一般是指某对象的所有的有效引用的个数,包括锁定计数器中的值,有的时候也特指位于对象附加控制块中的引用计数器的值。当联系到引用计数的值降到0时进行的处理,总是指包括锁定计数器和引用计数器在内的所有引用数。
下面将结合附图详细描述本发明的优选实施例一。
如图1中的图解,一个计算机系统可能是个人电脑(PC)、个人数字助手(PDA)、互联网装置、无线移动电话、服务器、或者其他任何计算装置。作为一个示范的例子,该计算机系统100包括一个主处理器单元101及其电源供给单元102。主处理器单元101包括一个或多个处理器103,并通过系统电路104连接到一个或多个内存单元105,一个或多个接口设备106通过系统电路104连接到处理器103。在本示范例子中,系统电路104是一个地址/数据总线。当然,一个所属技术领域的技术人员可以使用其他非总线的方式连接处理器103和内存单元105,例如使用一条或多条的专用线路,和(或者)纵横交换器,将处理器103和内存设备105连接起来。
处理器103可以包括各种微处理器,例如x86系列的Intel PentiumTM微处理器,AMD AthlonTM64微处理器等,或者PowerPC,ARM,MIPS等其他系列。内存单元105包括随机存储内存,例如DRAM(DynamicRandom Access Memory)等。在本例子中内存单元存放着被处理器103执行处理的代码和数据。接口电路106可以使用各种接口标准,例如USB、PCI、PCMCIA等。一个或多个输入设备107包括键盘、鼠标、触摸屏、语音识别装置等通过一个或多个接口电路106连接到主处理单元101。一个或多个输出设备108包括显示器、打印机、扬声器等通过一个或多个接口电路106连接到主处理单元101。系统还可以包括一个或多个外部存储单元109,包括硬盘、CD/DVD等。系统通过网络设备110与其他外部的计算机设备交换数据,网络设备包括以太网、DSL、电话线、无线网络设备等。
图2是一个本发明的实施例的结构框图,垃圾回收器200运行在图1所示的公知计算机上,由处理器103执行位于内存设备105中的代码和数据。垃圾回收器200可以作为用户模式(User-Mode)下的应用程序的一部分,也可以是操作系统内核的一部分,或者介于两者之间的平台组成部分,例如Java或.NET平台的虚拟机的组成部分,也可以是其他需要内存管理的系统的一部分。
在该图中,垃圾回收器由以下部分组成GC接口模块201、运行态的引用计数模块202、引用跟踪回收器203、GC同步管理器204、线程管理器205、和GC堆管理器206。而GC编译态辅助代码212则是分散在应用程序210的整个代码中,目的是获得最大限度的性能提升。应用程序210可以直接地、无限制地访问本地的堆和静态的数据段209。精心设计的后端接口则提供了平台无关的特性,令垃圾回收器200可以方便地移植到各种不同类型的平台上,只要这些平台支持虚拟内存服务208和一些其他系统服务207。系统要求的平台相关的服务211是如此的精简,适合大多数的通用计算环境。
GC接口模块201接受各种来自应用程序的GC函数调用,并调用其他相关内部模块的服务,完成应用程序提出的服务请求。
运行态引用计数模块202提供引用计数的服务,特别是回收引用计数为0的对象。当一个对象的引用计数变成0,该模块就在GC同步管理204的协调下回收该对象。
GC同步管理204是一个虚拟模块,它的代码分散在垃圾回收器200的各个内部模块中,这样的设计提高了关键代码的执行效率。
引用跟踪回收器203是整个系统中最重要的部分,它可以由应用程序显式地激发,也可以有系统线程周期性地激发或者在其他条件下激发。它扫描所有受管理的对象,找出活动的对象,跟踪遍历对象的引用关系图,收集和释放不可达的对象。
GC堆管理器206从虚拟内存系统服务208中分配页面,分割成小块的内存块并簿记这些内存块的使用情况,并同时提供写屏障(Write Barrier)和内部指针(Interior Pointer)的定位服务。
线程管理器205为其他内部模块提供平台无关的线程服务,将各个不同平台的线程服务统一到一致的函数调用,对于本身不支持线程的平台,则使用内建的线程库实现用户线程,这样对于其他模块线程的实现方式就是透明的、无须理会具体的实现。应用程序的线程可以通过该模块登记成“非GC”的线程,以免受垃圾回收的打扰。该模块提供线程暂停执行和线程恢复执行的调用,供引用跟踪回收器使用。
本地堆和静态数据段209是常规的malloc/free或者new/delete方式的堆,及静态的数据段,包括全局变量、本地变量、类变量等。任何类型的传统数据结构都可以自由地存储在这些区域,和传统的C/C++程序一样。然而,如果这些数据结构中存在引用GC堆中的受管理的对象,而且希望通过这些引用保持GC对象的存活,则需要在适当的时候调用GC接口函数以通知系统相关的引用变化,后文有详细说明。另外,如果应用程序需要GC系统跟踪到这些本地对象内部,则一些同步的GC接口函数必须适当地被调用。
接下来,为了更好地理解反映本发明的内在精神,将详细地分析和说明本发明的实质和实施例。
精确的引用跟踪回收本发明的一个实施例的目标是提供精确(Accurate,Exactness or Precise)的垃圾引用跟踪回收。精确的垃圾回收系统能够回收所有的垃圾而不会造成内存泄露,它要求能够确定所有的指向受管理对象的引用,包括来自GC堆内部的其他受管理对象,和来自GC堆外部的指针变量,例如来自线程的运行栈中的指针类型的自动变量。精确的垃圾回收有以下的优点o消除因为保守回收造成的某些对象不能回收,引起内存泄露;o允许编译器进行最大程度的应用程序代码优化,而不会象保守回收机制那样出现问题;o消除了对象句柄造成的额外的开销;o允许大量的不同的引用跟踪方法可以在此基础上得到应用。
一个标准的精确垃圾回收系统,它的GC回收操作不能随意地在任意的执行点进行,必须只有在所有的线程都允许GC操作的时候才能进行。垃圾回收操作只能在所有线程都是GC-Safe状态下才能进行。每个线程只能位于GC-Safe状态或者GC-Unsafe状态。处于GC-Unsafe状态的线程可以自由地使用堆中的对象,和随意地进行GC指针操作,例如指针运算等,由于GC系统对于GC-Unsafe状态下的线程,不能掌握所有的必须的引用关系信息,所以不能在此状态下进行GC引用跟踪。处于GC-Safe状态的线程则将所有它使用的、影响GC回收的引用关系信息披露了出来,所以GC回收操作可以进行。
通常,为了避免因为编译器的优化处理,例如使用寄存器保存指针,导致的部分引用关系不能被GC系统检测发现,有以下几种处理技巧线程劫持(Thread Hijacking)-编译器修改线程的运行栈,将函数的返回地址指向某个特别的函数,当函数结束返回时,该特殊函数就会被执行,并暂停该线程的执行等待GC回收处理。
插入安全点(Inserting Safe Point)-编译器在适当的地方加入对GC系统的调用,例如在一些长时间的循环过程中,GC系统则检查此时是否需要暂停该线程并开始垃圾回收。通过此方法来保证线程能够在一定的时间内到达GC-Safe的状态点。
分布表(Layout Table)-编译器生成一些数据表格,描述函数过程中每个点的指针使用情况,这样的函数就是完全可以中断的。此方法类似于程序的调试信息的产生,因此会带来较大的代码臃肿问题。
还有一些其他的方法,所有大部分的这些方法都需要编译器的支持,通过插入这样那样的代码,或者插入各式各样的描述数据以供特定的GC系统使用。然而,常规的C++语言并没有提供这样的支持,C++的标准也没有这方面的规定,例如如何生成或者生成怎么样的描述数据,而且这些数据的格式通常都与特定的GC回收器相关,没有也很难有统一的标准。所以,在C++的环境下,保守的垃圾回收方法占据了绝对的统治地位,但是保守的垃圾回收有很多在前面讲到的问题。
本发明提出一个创造性的方法,令常规的C++程序也可以使用精确的垃圾回收。整个过程中,无须进行应用程序的根集指针扫描,所以更谈不上保守地猜测类似指针的数据。而且,所有的应用程序代码自动变成GC-Safe类型,在任何时候都可以立即进行垃圾回收操作,而无须等待所有的线程进入GC-Safe状态。
方法的核心及抽象之后的版本是应用程序在运行时,动态标识出哪些对象是确定处于活动状态的,然后在引用跟踪回收过程中以这些对象为起点,跟踪遍历,确定不可达对象并予以回收。
如何标识出处于活动状态的对象,是方法的关键。下面详细说明采用的方法。
首先,定义“扩展根集”为所有非GC堆的区域。GC堆是指受管理对象的集合,不一定需要实际的连续的一段内存空间,后面描述的一个实施例就没有实际存在的GC堆。受管理对象是指通过GC系统分配的对象,并由GC系统维护和释放,例如用户自定义的结构可以通过gcnew或者类似操作,请求GC系统创建一个受管理的对象并返回它的相关地址。这样,扩展根集合就包括静态的数据段、线程的执行栈、处理器的寄存器、本地的堆、等。从程序员的角度,则包括全局变量、本地变量、静态变量、栈内的自动变量、函数的参数及返回值、还有位于上述区域及本地堆中的对象的成员变量。换句话说,就是除了受保护对象的成员变量之外的其他变量。
一个更加广泛的“扩展根集”的定义为所有将被引用跟踪所遍历到的指针或者有效指针不属于扩展根集,而其他的指针则属于扩展根集。这个定义比较难理解,但是更加准确,因为它包括了类似这样的情况,指针位于本地堆中,但是将通过某个属主对象,GC系统将跟踪该指针及其引用的对象。
所有来自扩展根集的引用将导致被引用的对象标志成活动的对象。例如,一个来自函数内部的、指针类型的自动变量,将导致该变量所指的对象标识成活动的状态。对于来自扩展根集的引用,系统应该进行监视,维护一致的状态关系至少是保守的一致的关系,即如果存在来自扩展根集的引用,则该对象需要被标识出来,有别于其他对象;反之,如果已经没有了来自扩展根集的引用,则该对象应该被取消标识,回归到其他的对象集合中去。保守的一致性是指,站在垃圾回收的角度,允许某些垃圾对象在本次回收操作中“漏”掉,(而在以后的回收操作中被收集,也可以不收集,只是过于保守了),那么,就可以在标识和实际情况上允许有一定的时间上的延迟,以提高效率。例如,当某对象已经没有来自根集的引用,该对象仍可以暂时在一定时间内保持着标识状态。
最直接的标识方法是使用引用计数方法,系统为每个受管理对象维护一个锁定计数器,该计数器代表来自扩展根集中的引用个数,同时作为该对象是否被标识的判断条件。如果增加了一个从扩展根集的引用,则该锁定计数器的值加1;反之,如果有一个来自扩展根集的引用超出了作用域(Scope),则锁定计数器的值减1;锁定计数器的值如果是正数,则代表该对象处于标识的活动状态,如果为0则表示该对象属于其他的类型。
引用计数的方法直接而且简洁,但并不是唯一选择,而且引用计数仍可以使用其他的变种,例如延迟引用计数(Deferred Reference Counting)、权重引用计数(Weighted Reference Count)等。在有必要的情况下,可以使用其他的方法,例如每个受管理对象对应一个结构,当扩展根集中有新的变量引用该对象时,系统判断该对象所对应的结构是否位于特定的集合中,该集合可以采用各种公知的管理机制,例如链表、数组、向量、聚集、集合、图、哈希表和及其组合等,系统维护该结构在集合中的唯一性,并以引用该对象的指针变量的地址作为依据,确保来自扩展根集的引用与该结构是否位于集合中保持一致。此方法开销比引用计数要大,仅作为参考,在本实施例中并没有采用。
具体的使用引用计数的方法如下系统提供了一组以template模板编写的智能指针,供用户编写应用程序使用。每种指针都针对特定的应用场景,包括使用该指针的目的,和使用该指针的变量的位置。也就是说,指向同一个对象的指针不再是C/C++的语法书中描述的那样的一种仅和对象类型相关的指针,而是可能有多种不同类型的智能指针。这也是本发明的一个特点之一,在后面的段落中有详细的说明。
这些智能指针当中,有一个名为“CLockedPtr”的智能指针,使用该类型的指针变量将总是保持所指的对象活动,而不会被回收。CLockedPtr指针缺省初始化为NULL,如果CLockedPtr初始化为某个对象的引用,则该对象的锁定计数器的值将加1;当CLockedPtr类型的指针变量离开作用域,则它所指向的对象的锁定计数器的值将减1;当将一个新的引用赋值给该智能指针变量时,将导致一个原位(In Place)析构调用及一个以新引用为初始值的原位构造调用,这将导致原来所引用的对象的锁定计数值减1和新引用的对象的锁定计数值加1。
整体来说,CLockedPtr智能指针在锁定计数器上完成了自动引用计数的功能。每个CLockedPtr变量就是一个该对象的锁的拥有者,而锁定计数器的值就是引用该变量的CLockedPtr指针的个数。
系统永远不会回收释放一个有非零的锁定计数值的对象,该对象总是作为活动的并且不会作为垃圾回收,运行态的引用计数模块也同样会严格遵循这个原则,锁定计数器的值也作为引用计数的一部分,只有锁定计数器为0的情况下,对象才有可能被回收。
图3A-C给出一个示意图说明在一个实施例中,一个受管理的对象的附加控制块及引用关系的示意。
图3A给出一个附加在用户自定义的数据结构之前的控制块。在其中一个实施例中,每个动态经过GC系统创建的对象300都有一个附加的控制块301,用户自定义的数据块302则尾随其后。当应用程序请求创建一个受管理的对象时,需要给出该自定义对象的大小及遍历函数等有关信息,在GC编译态辅助代码中有相关的机制帮助程序员方便地给出所需的信息,随后的段落中有详细描述。系统在创建了上述这样的对象结构并初始化控制块之后,返回地址303。应用程序通过该地址303按传统的C/C++的方法使用该对象,可以使用“Dirty”的原始的指针304访问对象的内部数据等等。附加的控制块301中,保存了一个锁定计数器和一个普通的引用计数器,还有一些其他信息,例如对象的大小和遍历函数的入口地址等。
图3B是一个示意图,代表一个受管理的对象。一个圆圈代表附加的控制块311,而方块则代表用户自定义的数据块312。
图3C是一个引用关系示意图,在图的中部有六个受管理对象编号从321到326,它们位于GC堆中。对象321通过指针327、329引用对象322和323,这些有效的引用使用实线标识,将会导致对象保持存活。对象321还有一个指向对象324内部的指针328,该指针一般不能有效地维持对象的存活,所以用虚线表示。
通常,指向对象内部的指针是不会被GC回收器跟踪的,不过应用程序可以在遍历函数中,先将指向对象内部的指针转换成指向对象头部的有效指针,然后再通知GC系统,这样系统就可以正确地保持该对象有效,并跟踪其引用的其他对象。某些实施例的内存分配器提供内部指针的定位服务,给定某个对象的内部地址,系统能查出该地址所属的对象。这种服务的实现方法有很多,有的通过保持一个页面内的对象拥有相同的大小,从而确定对象的开始地址;更多的方法使用卡片(Card)方式,进行一定范围内的保守的簿记,然后在此范围内扫描确定归属的对象。这些方法开销都比较大,目前没有较高效的令人满意的算法,所以应该尽量不要使用这种通用的内部指针定位方法,而应该由应用程序根据实际情况有针对性地处理,将内部指针转换成对象指针。例如使用COM的接口方式获得真正对象的地址,或者使用CONTAINING_RECORD宏将内部指针转成确定类型的对象地址。不过,如果运行环境不在乎效率,例如解释环境或者脚本语言,也可以使用这些通用的转换方法,牺牲效率以换取使用上的便利。
系统允许使用原始的脏(Dirty)指针和指针运算,这样既兼容了原来的C++代码又获得了相当高的执行效率。程序员需要自己保证正确地使用原始的脏指针,使用的原则很简单,就是保证存在有效的对象引用的情况下才能使用脏指针,一旦失去了有效的对象引用,该对象就会立即被回收,所有的相关脏指针就绝对不能使用了。也就是说,要注意不要随意失去对象的有效引用。举例说明,在图3C中,对象321因为存在有效引用327到对象322,如果应用程序确定对象322引用了对象324,则应用程序可以随意地使用脏指针328访问对象324;而指针330则是危险的,因为没有有效的指针指向对象325,对象325和326是循环引用的垃圾对象,它们将在下一次的垃圾回收操作中被回收释放。
本系统有一个优点,就是不需要插入特殊的数据结构到用户自定义的对象中,在图3A中,灰色的302部分完全由用户定义。而目前的Java和.NET的对象模式都要求用户自定义的类必须继承于某个基本的全局通用的类,例如Object类。这个特性给予了程序员更大的自由,可以选择某个特殊的基类去继承,或者根本不继承任何的其他的类。这样就和C++的语义相符,大量的过去的投资得以保护,例如,在本系统的支持下,微软的COM可以不失兼容性的情况下,改进支持垃圾回收机制。
引用跟踪的垃圾回收可以在上述的方法下实施。为了便于读者理解,下面先给出一个STW(Stop-The-World)类型的引用跟踪回收器的实现,之后再进一步给出增量回收等方法。
图4是一个“Stop-The-World”引用跟踪垃圾回收操作的流程图。首先,挂起所有的线程(步骤401);然后将所有的对象切换成“白”色(步骤402);扫描所有的对象,找出锁定计数为正数的对象,标记为“黑”色(步骤403);以这些锁定的对象为起点,跟踪遍历所有被引用的对象,将所有可达对象标识成“黑”色(步骤404);最后恢复挂起的线程,然后回收那些“白”色的垃圾对象(步骤405)。
在整个过程中,不需要等待线程进入GC-Safe状态,就可以直接挂起所有的线程;而且没有扫描根集的操作,来自根集的引用已经通过CLockedPtr智能指针将被引用的对象锁定(锁定计数不为零)。
由于引用计数的原子操作性,在任何时候,一个对象只能处于两个状态,锁定或者非锁定。配合正确的锁定和解锁次序,对象总是保持正确的状态,没有中间的过渡的不一致状态,GC系统总是获得正确的、一致的引用关系图。所以,全部的应用程序代码都支持GC,不存在GC-Unsafe状态,GC操作可以随时开始。如果一个线程激发GC操作,系统可以立即开始挂起其他的线程,开始真正的工作,不需要象以前那样等待应用程序线程进入GC-Safe安全点才能挂起线程,这是无暂停的增量垃圾回收的基础之一。
从另一个角度来看,对象的引用只能存放在两个地方,要么是在GC堆中,要么在GC堆之外。对于前者系统会按目前的技术进行跟踪遍历,而对于后者,这些引用已经在应用程序的正常运行的过程中引发了对象的状态发生了变化,标识出了这些受外部引用的对象。这样,垃圾回收操作就只需要处理GC堆内部的数据就可以了,引用关系图从整个应用程序转化成GC堆内部范围。以GC堆的边界划分垃圾回收的工作区域,避开了外部复杂的情况,例如不需要考虑线程运行栈内的指针分布和处理器的寄存器使用情况。
在本实施例中,应用程序仅仅是通过锁定计数的方式标识出被外界所引用的对象,由垃圾回收时再通过扫描确定锁定的对象,作为遍历的起点。另一种方法是,加大应用程序执行时的动作,维护一个起始对象集合,将锁定计数器的值为正数的对象加入到该集合中,在对象的锁定计数器变为0时,又将该对象从该集合中剔除;在引用跟踪的垃圾回收的时候,系统直接使用该起始对象集合作为引用跟踪的起点。这样,可以减少垃圾回收时的开销,无须扫描所有的对象。对于图4的流程,只需要将步骤403去掉或者改为直接对该起始对象集合处理即可。
图5A-C示意了一个对CLockedPtr智能指针的赋值操作,以便理解。
图5A是初始化状态,CLockedPtr 501位于GC堆500之外,缺省初始化为空NULL;在GC堆500内,对象502通过指针504引用对象503,(假设对象502有其他指针引用它,从而保持其处于活动状态)。因为对象503仅由对象502所引用,其锁定计数器的值为0而引用计数器的值为1。
图5B示意在赋值发生之后,指向对象503的引用504赋值到CLockedPtr 501智能指针上。由于原来CLockedPtr 501的值是空NULL,不需要将原来的对象的锁定计数值减1;智能指针501现在有了一个指向对象503的引用505,并且自动将对象503的锁定计数器的值加1;最终结果是对象503的锁定计数器的值为1且引用计数器的值为1。
图5C示意在赋值之后,原来的变量不再引用该对象了。指针504从图中移除了,对象503的锁定计数器的值仍为1而引用计数器的值为0,对象仍然有效。
对象锁定计数器的维护确实带来了一些运行时的开销,然而由于这些开销通常发生在赋值操作的时候,而且绝大部分的操作是发生在线程栈中。对于一个以函数为主体的编程环境,这些开销是可以消除的,下面将详细说明如何去除这些开销,并说明确定性的回收及析构的方法。
确定性的回收机制本发明的一个实施例在上述的垃圾回收方法的基础上提供了确定性的垃圾回收机制。
确定性的回收机制的主要优点体现在资源的管理上。目前的垃圾回收方法不能保证对象能及时地释放,所以需要程序员手工地显式释放对象所占据的其他非内存的资源。通过确定性的回收机制,资源的管理变得清楚和简单,资源在对象的构造过程中分配,在对象的析构过程中释放,即用对象来代表资源。系统在尽可能早的时期就将不可达的对象回收,释放相关的资源。更确切地说,如果一个对象失去了所有的引用,则在最后一个引用失去的时候,系统会自动回收该对象。
另一个优点是确定性的回收不仅调用析构函数,而且还回收了对象所占用的内存。因此,内存得以尽可能早的回收,内存的利用率大大提高了,引用跟踪回收操作的周期可以大大延长,执行效率得到提升。如果一个应用程序得到精心的设计,去除了循环引用的数据结构,则可以完全不需要执行引用跟踪回收操作,去除了该操作带来的昂贵开销。(当然,有些人可能比较倾向一次过地回收所有的被垃圾占据的内存,这只需要将确定性回收时释放内存块的操作去除即可,只执行对象析构函数,垃圾所占据的内存由系统在引用跟踪回收操作中一次过地回收。)从内存利用率的角度,应用程序的内存使用量的变化在使用确定性回收之后,要比之前更加平滑,内存使用量不再是持续地上升直到垃圾回收操作,而是尽早地释放了,因此提供了更高的内存使用率。
引用计数的方法可以提供一个很好的确定性的回收机制,本系统正是利用了这一点,为每个对象维护一个锁定计数器的同时,还维护一个引用计数器。来自非扩展根集的引用将导致引用计数器的值的变化。当两个计数器的值都到达零的时候,即意味着整个系统中没有指向该对象的有效引用了,系统便将该对象回收。
问题是引用计数尤其是锁定计数的维护开销比较大,因为它的引用来自访问频繁的根集区域,特别是在线程的执行栈内。如果采用延迟引用计数的方法,虽然效率提高了,但是它失去了确定性回收的性质,因此并不适用。本发明提出了一种新颖的提高引用计数器效率的方法,描述如下。
正如上文所述,在本发明的其中一个实施例中,引用计数整合在引用跟踪回收的系统中,引用跟踪利用引用计数来标识出跟踪遍历的开始对象。系统提供一个头文件(Header File),定义了有关的辅助用的宏(macro)、模板(template)和函数等。应用程序的源代码可以引用该头文件,在编译的时候将这些代码嵌入(inline)到应用程序的代码中,通过使用编译器的优化功能,可以消除绝大部分的不必要的代码执行路径。在运行时,引用计数模块将系统内部的数据与应用程序分隔开,例如对象的附加控制块的内容对于应用程序来说就是透明、不可见的。而引用计数模块则直接使用引用跟踪回收器的内部数据及其他内部数据,在正确的同步管理机制下,协调来自不同线程的并发访问。
提供高效的引用计数的关键在于以下的事实,大部分的引用计数器的更新操作都发生在栈内指针变量的操作中,而这些变量可以分成两类,一类将导致被引用的对象的存活,而另一类则不能。例如图6列出的伪代码。
行号001-003,声明了一个用户自定义的类“CMyClass”,及两个函数“funcB”和“funcC”;行号004-012,定义了函数“funcA”;行号013-018,定义了函数“funcC”,而函数“funcB”则是外部函数,在其他模块中定义;假设“funcA”被调用,参数是一个指向对象的引用,在函数“funcA”的执行过程中,该对象保持有效。行008定义了一个自动变量t2并在行009得到函数“funcB”的返回值。行010调用“funcC”,参数是两个输入性质的对象引用,及一个变量地址以接收对象引用的返回值。
在此例子中,所有的输入参数包括“pInObj”(行004),“pIn”(行013),“pIn2”(行013),它们都不需要保持所引用的对象存活。自动变量则根据其用途,有些需要保持所引用的对象存活,有的则不需要。例如,在函数“funcA”中,变量“retval”和“t2”需要保持所引用的对象存活,而变量“t1”则不需要。正如前文所述,智能指针“CLockedPtr”将导致它所引用的对象存活,用CLockedPtr智能指针改写图6的伪代码,改写部分列在图7中。
在图7中,函数的原型作了一些修改,所有的从函数返回的指针类型均用CLockedPtr智能指针替换,例如行002,004,和013。自动变量需要保持对象存活的也用智能指针进行替换,如行006和008。注意这样的事实,行007中定义变量“t1”没有变化。
经过这样的修改之后,产生的伪代码已经很接近最终的结果了。大部分的指针变量不需要使用智能指针进行替换,访问和赋值不会导致这些指针所引用的对象发生引用计数的变化,这些开销被消除了。一般情况下,函数的输入参数无需保持对象存活,而输出的参数和返回值则通常需要保持对象存活,函数内部的变量则视具体情况而定。
系统通过CLockedPtr智能指针进一步地消除了引用计数的操作。到现在为止,引用计数的维护开销来自两个方面,一个是来自受管理对象的成员变量,另一个是来自CLockedPtr智能指针。一个新的智能指针“CMemberPtr”被引入,该智能指针用于替换受管理对象的成员指针变量。CMemberPtr智能指针只应该出现在类的成员变量中,而且该类的实例应该只作为GC系统创建的对象,属于GC堆的一部分,或者本地堆的对象并附属于某个GC对象,接受引用跟踪。(作为静态段或者线程栈的一部分被引用跟踪也是可以的,不这种情况极其少见,而且应该避免)CMemberPtr辅助CLockedPtr消除了引用计数的开销,而且在无暂停的增量回收中与CLockedPtr一起发挥了关键的作用。当一个引用赋值给CMemberPtr,系统自动将原来引用的对象的引用计数器的值减1,将新引用的对象的计数值加1;指针离开作用域之后,原来引用的对象的计数值自动减1;如果引用计数和锁定计数都为0,则系统执行该对象的析构函数,并可能导致更多的对象回收。
而作为CLockedPtr智能指针,它的实际功能还不只是前面所描述的保持对象存活和标识遍历的起始对象,系统根据不同的赋值操作的“右值”类型,使用不同的策略。“右值”包括赋值操作的参数,和初始化过程的参数。
给定一个对象的引用,可以分成三种类型并属于特定的区域1)原始指针或者脏指针,完全等同C/C++的指针,没有任何智能的动作,该类指针可以用于任何的区域;2)CMemberPtr智能指针,用于GC堆或相类似功能的区域;3)CLockedPtr智能指针,用于扩展根集中。
当将原始指针赋值给或初始化CMemberPtr或CLockedPtr智能指针时,系统将自动将原来引用的对象的引用计数器或锁定计数器的值减1,将新引用的对象的相应计数值加1。当用原始指针或CMemberPtr赋值或初始化CLockedPtr时,对象的锁定计数值加1。
然而,当CLockedPtr赋值或初始化操作是以另一个CLockedPtr指针作为参数时,系统先将原来引用的对象的锁定计数器的值减1,然后将新对象的引用“移动”到该智能指针中,并不进行锁定计数器的更新操作,“右值”变量中的引用被清空。也就是说,CLockedPtr智能指针类型的变量之间的赋值操作更象一种引用的传输操作而不是一种拷贝引用的操作。
CLockedPtr额外提供一个名为“Duplicate”的成员函数,它返回一个克隆的CLockedPtr引用。调用该函数将导致锁定计数器的加1,而且原来的智能指针的值不发生变化。这样,如果程序员需要原来那种导致引用数增加的赋值操作,他/她可以按以下伪代码编写程序p2=p1.Duplicate();其中p1,p2是CLockedPtr类型的智能指针变量。一般来说,在一个函数的代码中,一个对象只需要一个CLockedPtr类型的智能指针就足够了,过多的克隆的CLockedPtr指针只会增加运行的开销,因此应该避免。事实上,需要多个CLockedPtr智能指针来引用一个对象,这种情况是非常少见的(如果真有这样的必要)。
某些时候,C++的编译器会自动创建临时的智能指针对象,由于CLockedPtr的拷贝构造(CopyConstructor)函数和赋值操作符(Assignment Operator)函数都已经重新定义了,缺省的操作已经改成了移动引用,不会产生锁定计数的更新操作。所以,整个引用计数的开销大大地降低了。
回到图6的伪代码和图7的修改,最终的伪代码如图8所示。图8中,存在三处显式或隐藏的、在CLockedPtr智能指针之间的赋值操作,分别是行009,010,016和011。这些赋值操作都没有引起锁定计数器的维护操作,也就没有相关的开销。给一个极端的例子,假设函数A1调用函数A2,A2调用A3,如此以往,最后函数A100调用库函数CreateObject创建一个对象,然后逐级将该对象的引用返回给调用者,最后回到函数A1,这整个过程不会产生引用计数器的维护开销。
图9A-B是一个示意图,说明一个对象的引用是怎样从函数中返回的。图9A,在线程的运行栈901中,智能指针902拥有对象904的锁定引用;指针指针903位于调用者的栈结构(Stack Frame)中,可以是图8中的016行,也可以是图8中的009行所示。在后一个例子中,事实上有两次的赋值操作,一个是构建一个临时智能指针对象,随后再将该临时对象赋值给变量“t2”。图9B是赋值之后的结果,对象的引用从智能指针902移到了智能指针903上,而对象的锁定计数器的值并没有发生变化,也不需要引用计数器的更新操作。
智能指针CLockedPtr和CMemberPtr可以输出原始指针类型的引用,该操作不会导致引用计数器的更新操作,没有产生引用计数的维护开销。
至此,减少引用计数的开销的方法已经全部说明清楚了,该方法不仅可以象本实施例中这样和引用跟踪的标识操作合并在一起(通过CLockedPtr智能指针),还可以独立应用于其他的环境中,只要是基于函数类型的编程模式下的使用引用计数,通过该方法就能收到较好的效果。对于CMemberPtr智能指针可以进一步的优化,减少引用计数的开销,例如使用类似CLockedPtr的移动引用的操作,在移动的过程中注意完成Write Barrier的簿记和注意内存访问次序的维护即可。又例如可以使用1bit的引用计数器,减少受管理对象之间的引用关系的维护操作的开销,受管理对象一旦引用其他对象,则导致被引用对象的计数器(1-bit)置位,该置位操作不需要原子操作(Atomic),因此在多处理器的环境下可以提高并发的效率。系统还可以根据不同的情况和功能提供多个智能指针,方便使用并提供更加细致的管理信息。例如可以分别提供针对全局指针变量的智能指针,以区分线程栈内CLockedPtr;或者同时提供总是进行锁定计数器维护的智能指针。各种方法都可以应用,只要遵循标识锁定对象的这个精神,正确维护好锁定计数器就可以了。
下面将要描述如何执行对象的析构函数。
一般来说,目前常规的引用计数机制是在应用程序中维护引用计数,当引用数为0时,直接执行析构函数并删除对象。析构函数执行时是带类型参数的,也就是说,调用析构函数的代码是确定对象的类型的。但是本系统中使用了引用跟踪机制,这种方法首先便无法实现,为了使跟踪回收能够正确地执行适当的析构函数,必须动态地提供对象的信息包括析构函数的入口地址。
本系统通过在每个对象的附加控制块保存一个指向VClassInfo的接口的指针,完成动态对象信息的提供。VClassInfo是一个类似COM的接口,通过它系统可以获得所需的有关该对象的类的信息,并且方便以后的扩充,将来的其他接口也可以从此接口中获得。VClassInfo接口可以在本系统提供的辅助代码的帮助下,由应用程序的类自动生成,应用程序需要提供一个Traverse遍历函数用于引用跟踪,而析构函数的入口系统则能够自动确定,对用户透明。事实上,只要能够提供动态的对象信息即可,上述的方法仅仅为其中的一个例子,例如如果每个对象都能够保证提供一个COM类似的接口,则对象的附加控制块中就可以不必另外保存VClassInfo指针,可以直接请求对象的接口提供有关的对象信息,这样就节省了一个指针大小的内存空间。同样使用语言本身的动态信息机制也是可以的,例如C++语言的RTTI机制,使用的方法类似COM的接口,仅仅将接口改为C++的虚拟基类。
有了对象动态信息的支持,给定一个对象的地址,系统可以获得该对象的有关信息,包括对象的析构函数入口,遍历函数(Traverse)的入口。这样,对象的析构函数就不再由应用程序自己调用了,而是由系统通过动态信息机制进行调用,例如通过引用跟踪回收器和运行态引用计数模块完成。在一个多线程的环境下的实施例,析构函数可能被其他线程异步地执行,例如某个线程执行引用跟踪回收操作将该对象回收。
对于循环引用的垃圾对象,由于总是存在有效的、指向该对象的引用,本实施例尚未能立即回收这些对象,本实施例只能做到在对象的最后一个引用消失时,立即回收。通过引用跟踪回收,这些循环引用的垃圾最终将被回收,但是由于是循环引用的,回收的次序无法确定。也就是说,应用程序不应该依赖析构函数的执行次序。也许将来有更好的方法指出析构的次序,例如通过创建对象的时间为依据,但是没有逻辑上的依据支持这样的回收次序;而依赖应用程序提供析构的次序似乎也不太可能,如果一个应用程序不能避免循环引用的数据结构,却能够提供这些垃圾对象的析构次序,这种可能性很低。
另外,在传统的C++编程习惯中,析构函数通常删除该对象所使用的子对象。在本系统中,一个对象可以拥有受管理的子对象和不受管理的、本地的子对象,直接删除后者是可以的,但不应该直接删除受管理的对象,而是应该清除指向该子对象的引用,由系统来完成子对象的回收。当一个析构函数执行的时候,它可以做的是清除指向受管理对象的引用,和释放其他本地的资源(native resource),包括本地堆中分配的对象。这样做的原因是受管理的子对象可能被其他对象所引用,所以不能直接删除;也因为该对象及其子对象可能被循环引用的对象所使用,成为循环引用的垃圾的一部分,而循环引用的垃圾对象的析构函数的执行是不确定次序的,子对象在析构时有可能已经提前被回收了。所以,在析构函数中不能认为子对象是存在的,不能随意地使用子对象的成员变量和成员函数,除非应用程序能够在其他逻辑结构上保证子对象是有效的,例如有全局的有效的智能指针指向该子对象。
如果应用程序确实需要一个严格的析构函数执行次序,可以这样显式地清除某些引用,令某些对象先变成垃圾对象,然后保持其他对象有效的情况下回收这些垃圾,应用程序可以等待直到这些垃圾已经回收了,然后再回收其他的对象。通过将要回收的对象分成几组,分批进行回收达到有次序地执行析构函数。本实施例提供了系统服务调用,能够保证当调用完成的时候,之前的垃圾已经被回收。该系统服务实施的方法是1)首先判断当前是否有一个垃圾回收操作正在进行,有则等待其结束;2)此时所有的引用数为0的对象已经回收了,剩下的是循环引用的垃圾对象;3)启动一个新的垃圾回收操作,将之前的垃圾全部回收。注意,在垃圾回收操作过程中变成循环引用的垃圾对象,有可能不被当前回收操作所回收,这是引用增量跟踪垃圾回收方法的保守特性所决定的,所以上述步骤需要启动第二个垃圾回收操作。
对于已经检测出来的、循环引用的垃圾对象,系统可以这样进行析构函数的调用。1)首先进行第一遍处理,执行每个对象的析构函数,在执行之前先保证该对象的引用数不会下降到零,例如引用计数先加1;2)析构函数的执行过程中可能造成其他的对象的引用数降到零,可以直接回收这些零引用的对象(执行析构函数并释放内存)并从垃圾列表中剔除;3)进行第二遍处理,将这些仍位于垃圾列表中的对象的内存进行回收。
同步机制与增量垃圾回收本实施例提供了一个增量的引用跟踪回收器,它基于更新写屏障(Update Write Barrier)方法。GC堆管理器通过操作系统的服务提供写屏障的功能。所有的在跟踪遍历过程中发生修改的对象,都将被检测出来,这些对象中的“黑”色对象则被改回“灰”色,并重新跟踪遍历。该方法存在一定的线程暂停要求,但是比起“Stop-The-World”方法暂停的时间要小得多,暂停时间比目前的其他一般增量垃圾回收方法也要小。
基本的增量垃圾回收过程可以描述为遍历对象的引用关系图,并标记不同的颜色。对象逻辑上首先标记成“白”色并等待回收,到遍历的结束时,该保留的对象将标记为“黑”色,如果没有可达的对象等待标记为“黑”则遍历结束。“灰”色意味着该对象是活动的但是它所引用的对象尚未标记和遍历。
与此同时,应用程序正在并发地执行着,某些对象的引用数可能下降到0。第一种处理方法是简单地跳过回收操作,等待引用跟踪回收器来完成这些对象的回收。虽然,这些对象将最终被引用跟踪回收器回收,但是由于几乎所有(如果不是全部)的增量垃圾回收器都是保守的垃圾回收方法,也就是说,某些在回收过程中变成垃圾的对象可能不能被回收,而被当作活动的对象保留下来了。(不过,在回收过程开始之前就是垃圾的对象,则总是可以在这次回收过程中正确回收的)。因此,如果某对象的引用数降到0,跟踪回收过程可能保守地将该对象当作活动对象,而只能在下一次的垃圾回收操作中才被回收。因为两次垃圾回收操作的间隔期可能很长,所以这些对象有可能被保留一段相当长的时间,这种处理方法不建议使用。
方法二,它保证了在垃圾回收结束的时候,其间引用数变为0的对象保证得到回收。应用程序可以调用一个系统函数,如果有一个垃圾回收过程在执行,则该函数将等待该回收过程完成才返回;如果没有并发的垃圾回收过程,则立即返回。这样,应用程序就可以保证在调用该函数之后,所有在此之前引用数就为0的对象已经得到回收,析构函数得到执行。析构函数的执行次序是不确定的。
可以这样实现,维护一个零引用对象的列表,(也可以使用其他“常规集合技术”),当垃圾回收操作过程中,某个对象的引用数降为0,则将该对象记录在该列表中,跳过对象的回收操作,继续应用程序的下一步工作。当跟踪遍历结束之后,这些记录在列表中的零引用对象将被回收和析构。由于该列表被多个线程共享访问,适当的同步机制必须采用,以保证列表的数据完整。
方法三,可以在垃圾回收的过程中直接进行零引用对象的回收。这种方法在实施例2中有详细说明。
前面提到每个对象在提供动态对象信息的时候,需要提供一个遍历(Traverse)函数,系统会在垃圾回收的过程中由回调该函数,它的作用是描述该对象引用其他对象的情况,具体地说,就是指出该对象在当前时刻引用哪些对象。实际操作中,程序员并不需要直接编写该函数,而是在一些宏定义的辅助下完成遍历函数的定义,下面是一段示范的C++代码class MyClass{...}; /*用户自定义的类*/HNXGC_TRAVERSE(MyClass){ /*定义MyClass类的Traverse函数*/HNXGC_TRAVERSE_PTR(m_pNext);/*指出本对象引用m_pNext指出的对象*/… …/*其他被该对象引用的对象地址*/}可见,程序员只需要正确地使用本系统提供的宏即可方便地定义出遍历函数。而且,HNXGC_TRAVERSE_PTR宏只要求一个被引用的对象的地址的值,即一个“右值”,并不需要是一个完整的指针,因此只要遍历函数可以从对象的成员变量中,推算得出它所引用的对象就可以了。例如,类MyClass可以根据它的某个成员变量调用某些函数,得到一个被引用的对象地址,然后以此地址作为HNXGC_TRAVERSE_PTR的参数即可。所以,只要有正确定义好的遍历函数就足够了,系统并不需要知道对象内部的结构,对象可以有由简单到复杂的各种结构,包括位域(Bit Field)、联合(Union)等过去属于隐藏指针的,不能被一般的垃圾回收方法所处理的结构。
本地的数据结构也可以通过遍历函数进行描述,由系统自动完成跟踪。举一个例子,假设一个受管理对象,它包含一个指向位于本地堆中的哈希表对象,该哈希表对象是纯的C++的对象,它可能在本地堆中创建和管理多个其他子对象。哈希表可以加入对其他受管理对象的引用,每次增加这样的引用的时候,应用程序调用GC接口函数,增加所引用对象的引用计数;在哈希表中剔除引用时,则减少相关对象的引用计数;在遍历函数中,则应该调用该哈希表的成员函数,枚举所有引用的对象,然后再将这些对象通知GC系统;析构的时候,则先枚举所有引用的对象,并减少这些对象的引用计数,然后再删除该哈希表对象。
要注意的是,遍历函数是由其他线程异步地调用的,一般情况下是线程安全的,但是如果受管理对象采用了动态的数据结构,而且遍历函数的正确执行必须依赖这些动态结构,则需要严格地考虑多线程的竞争问题。应用程序在修改这些动态结构的时候,需要禁止该遍历函数的执行,这不能使用一般的同步机制,因为遍历函数执行之前需要暂停其他线程的执行,如果遍历函数等待某个挂起的线程的同步资源,则会造成死锁。为解决此问题,系统提供了一个专门的排它锁及锁定服务,当应用程序需要修改某些关键的数据结构时,而且这些修改会造成不一致的数据关系,并影响遍历函数的正确执行,则可以通过调用该函数,禁止遍历函数在此期间执行,同样当垃圾回收操作处于关键的阶段,例如将要暂停所有线程作最后的遍历的时候,将锁定该锁。因此,当应用程序成功获得该锁时,垃圾回收过程一定不会执行遍历函数,也不会执行暂停线程等可能造成死锁的操作,系统会等待直到应用程序释放该锁。在上述的哈希表例子中,由于遍历函数需要一致的哈希表状态,所以哈希表的成员函数的操作都必须使用这种锁进行保护。(除非应用程序保证总是显式地执行垃圾回收操作,并且在执行期间不会发生哈希表的访问。)为了配合增量的垃圾回收,对于上述的跟踪到本地对象中的情况,应用程序还在引用关系发生变化的时候,调用一个专门的函数,通知GC系统。该函数指出发生引用变化的对象,以便系统能够采取相应的动作,维护跟踪遍历的正确性。该函数必须在增加引用之后,而原来的引用变量仍保持有效的情况下进行。该函数的调用是写屏障的变型,函数的实施与引用跟踪的方法密切相关,本实施例中,它将导致该发生修改的对象从“黑”变“灰”,等待重新遍历。而在实施例2中,该函数为空函数。
多个并发的垃圾回收请求也应该使用同步机制进行保护,本实施例中只允许一个垃圾回收操作在执行;多处理器的环境下,可以采用更加细致的同步管理,允许多个线程并发地进行垃圾回收。
图10A是增量垃圾回收的流程图,图10B是图10A中确定锁定对象步骤的详细流程图。
步骤1001试图锁定一个全局排它锁,以保证只有一个实例在运行垃圾回收操作。如果已经存在一个正在运行的垃圾回收操作,则等待该回收操作完成,然后结束返回。
步骤1002将所有的对象切换成“白”色。通过改变标志所代表的含义,该操作很快且为常数复杂度。
步骤1003、1004、1005暂停所有的线程(除了显式声明为非GC线程),然后调用GC堆管理器开始一个写屏蔽监视,然后恢复线程的执行。从此之后,所有的对GC堆的写操作都将记录在下来。
步骤1006扫描所有的受管理的对象,找出那些锁定的“白”色对象,将他们送入“灰”色集合等待遍历。该操作的复杂度正比于受管理对象的个数,所以,可以在扫描过程中,每完成一定工作,例如每次扫描了20个对象,就释放一次控制权,令应用程序可以创建新的对象。本操作的目的是将锁定的对象加到“灰”色的集合中,各种方法都可以完成该步骤,例如可以由应用程序动态地将锁定的对象加到某个特定的起始对象集合中,而在此步骤中则直接将起始对象集合送入“灰”色对象集合。
步骤1007以“灰”色对象为起点,跟踪遍历所有的对象。可以这样实现从“灰”色对象集合中逐个取出对象,调用对象的遍历函数;如果该对象所引用的对象是“白”色,则将该“白”色对象加到“灰”色对象集合中,等待下一次遍历;调用过遍历函数的对象则送到“黑”色集合中;不断地从“灰”色集合中取出下一个对象进行处理,直到“灰”色集合为空。
如果整个遍历过程中处理的对象的个数很少,满足预设条件则跳到最后的处理阶段(步骤1010),而无须进行增量的跟踪遍历。
步骤1008开始增量的跟踪遍历,直到条件满足进到步骤1010进行最后的处理。增量跟踪遍历的过程是回收器尽量扫描更多的“白”色对象到“灰”色,跟踪遍历令“灰”变成“黑”的过程;而应用程序则不断修改引用关系图,捕获的这些修改并导致“黑”色对象的重新变“灰”,重新跟踪遍历的过程。
步骤1008类似于步骤1006,区别在于扫描锁定对象的过程中不再释放控制权,并保持对象集合的锁定,因此在此过程中,不允许有新的对象创建。
步骤1009指令GC堆管理器报告从步骤1004或上一次执行步骤1009,以来发生过修改的对象。该步骤是在暂停所有线程的情况下调用的,修改过的对象如果是“黑”色则需要重新加到“灰”色集合中,重新跟踪遍历。
步骤1010开始最后的处理阶段,通常当回收器的速度赶上应用程序,或者系统发现回收器很难赶上应用程序时,将进入最后的处理阶段。此阶段将暂停所有线程,并在暂停的状况下完成跟踪遍历的操作,因此在处理过程中不会有并发的引用关系的修改。此步骤的开销不是想象中的那么大,因为绝大部分的对象已经改成了“黑”色,跟踪遍历的范围要小得多。
步骤1011回收所有的垃圾对象,并释放排他锁,结束退出。而其他阻塞在该锁上的并发垃圾回收操作也将自动被唤醒而返回。
在整个增量回收操作的过程中,有几个地方是需要暂停应用程序线程的,其中开销最大的处理是在最后阶段的跟踪遍历。下面将要描述另一种增量垃圾回收的方法,可以完全避免暂停应用程序线程。
优选实施例二在图1所示的公知计算机内,具备处理器和内存设备,可以运行预先编制的程序,包括二进制代码、P-Code等中间代码、及源代码等。图11是本发明的实施例的结构框图,垃圾回收器1103运行在图1所示的公知计算机上,作为内存管理的系统的一部分。垃圾回收器1103包括以下部分GC接口模块1104、运行态的引用计数模块1106、引用跟踪回收器1105、和虚拟GC堆模块1107。而GC编译态辅助代码1102则是分散在应用程序1101的整个代码中。系统直接从本地堆1108中分配受管理对象。
本实施例的绝大部分与实施例一相同,因此如果没有特别的指出,可以认为和实施例一是相同的。从结构框图可以发现主要的部分,例如引用计数模块1106、引用跟踪回收器1105,都依然存在。但是也会发现,本实施例比实施例一要精简了若干组成部分,包括线程管理模块205,GC堆管理器206等。这主要是因为本实施例使用新的写屏障机制,从而不再需要特定的GC堆管理器来分配内存,以提供写屏障的服务。实施例一是通过GC堆管理器206,进行受管理对象的分配和释放,并在操作系统的帮助下提供写屏障的服务的,它依赖于虚拟内存机制和特定的内存分配或簿记机制。而在本实施例中,系统是通过引用计数器的维护操作同时完成了写屏障的功能,不需要特定的GC堆分配方法。事实上,本系统直接使用本地堆来创建受管理对象。虚拟GC堆模块1107是一个逻辑上的模块,它提供一些必要的管理功能,例如列出所有受管理的对象。
使用引用计数器的维护代码来完成写屏障的好处是避免挂起应用程序线程。另一个好处是不再要求操作系统提供SuspendThread、GetWriteWatch这类的系统服务,甚至连虚拟内存的服务也可以不用提供。这样,系统的适用范围就更加广泛了。再一个好处是这种写屏障是指针修改操作时立即发生的,它比使用GetWriteWatch或者虚拟内存的写保护等机制要更加强大,它可以直接使用各种同步机制而不用担心因为挂起线程而造成死锁。
在一个指针的赋值操作中,涉及原来所引用的对象的计数值递减,新引用的对象的计数值递增,在引用计数递减操作中可以实现Snapshot-at-beginning增量回收所需要的写屏障,在引用计数递增操作中可以实现Incremental update增量回收的向前变种的写屏障。前者需要记录下增量回收操作期间,被改变的指针的原来内容(即原来引用的对象),后者则需要记录下新引用的对象。对于Incremental update增量回收的滚回变种方法,则可以通过记录下指针本身所属于的对象而实现。(实施例一采用的就是Incremental update增量滚回的垃圾回收方法。)上述中的引用计数器显然包括锁定计数器和引用计数器,因为来自对象成员变量的引用和来自扩展根集的引用都同等重要。本实施例以Incremental update增量回收的向前变种作为基础,配合1)使用标识锁定的对象,避开了根集指针的扫描操作;2)所有代码自动GC-Safe;3)包括根集指针在内的写屏障支持;加以改进之后,提供了一个完全没有线程挂起操作的增量回收方法。下面是对改进的详细说明。
引用跟踪回收器1105现在更加紧密地与引用计数模块1106相关联,引用计数模块1106可以直接访问引用跟踪回收器1105的内部数据结构,当然必要的同步机制是不可以少的。基本的原则没有变化,仍然将指针分成三类,原始指针、CLockedPtr和CMemberPtr;锁定的对象仍作为跟踪遍历的开始对象;二进制的GC接口没有变化,兼容实施例一的应用程序二进制代码。垃圾回收操作分成两个阶段,标记(Mark)阶段和回收阶段,在此过程中,应用程序可以不间断地修改引用关系图,无须暂停。一旦垃圾对象被确定,就可以“从容”地回收这些垃圾对象,不必担心与应用程序发生竞争,垃圾是不会被应用程序所访问的。应用程序对引用关系图的修改被引用计数模块所捕获,在本实施例中,只捕获赋值操作的“右值”对象,具体地说,就是锁定计数器和引用计数器的加1操作被捕获,操作的目标对象被记录。一个专门的函数用于处理捕获,称为“赋值修改”(Assignment Mutator)。从回收器的角度,应用程序异步地调用该函数。
图12归纳了回收操作中的标记阶段的流程。
先有这样的前提假设,1)系统保证只有一个垃圾回收实例在运行;2)全部的有效引用都应该正确地调用系统的引用计数操作,正如智能指针所做的那样;3)系统能够提供受管理对象的列表;4)在此期间引用计数为0的对象暂不讨论,后续有详细的分析。
步骤1201,完成跟踪遍历的准备工作,包括1)获得访问内部数据结构的保护锁;2)设置内部数据状态,例如切换所有对象成“白”色,设置标志指出标记工作的开始;3)释放保护锁。此步骤完成之后,所有对象变成“白”色,其中有一部分拥有正的锁定计数值,而引用计数器的值则不予考虑。
步骤1202是最主要的,它的工作是将“白”色的锁定的对象转成“灰”色,然后调用“灰”色对象的遍历函数,将该对象所引用的“白”对象转成“灰”色,完成遍历函数的调用之后,将“灰”对象改为“黑”。在此期间,并发的应用程序的赋值操作将被捕获,而导致更多的“白”对象改为“灰”等待处理。本步骤必须不断地处理“灰”对象,执行其遍历函数,改为“黑”色,直到没有“灰”色的对象。
步骤1203检查是否存在“灰”色对象,如果该检查涉及与应用程序共享内部数据结构,则需要锁定保护。如果还有“灰”色对象则继续步骤1202,没有则继续下一步骤。
步骤1204一旦程序执行到本步骤,意味着没有“灰”色对象了,可以推导得出此后不可能再出现“灰”色对象,所以,标记工作到此结束,所有对象被分成两组,“黑”的和“白”的。
新创建的对象在初始化之前不会引用任何其他对象,随后初始化中的赋值操作将被系统捕获,所以,在步骤1202期间新创建的对象不需要执行遍历函数,可以直接标记成“黑”或者“白”。本实施选择标记成“黑”,这样可以保证系统中的“白”对象的数量不会增加。
对于赋值操作,系统不仅仅是按传统的写屏障将新引用的对象记录下来,而且通过同步的机制与回收器进行交互,形成多线程的处理并发处理,这样就无须使用挂起线程的操作,而只要使用同步机制短时间的在竞争时阻塞即可。整个增量回收过程便成为了一个回收线程与多个并发的赋值操作和创建新对象操作的多线程同步关系。
结合这两种并发的操作,步骤1202可以描述成这样垃圾回收系统扫描所有对象(新创建的对象除外),将“白”色的缩定对象改为“灰”色;扫描的次序和方法可以各种各样,并且可以夹杂着其它的操作,例如调用对象的遍历函数,只要能够全面地、完整地将步骤1201之前的所有的对象都扫描一遍就可以了。扫描过程中,可能有些原来锁定的对象又变成的非锁定的,由于赋值操作已经被系统捕获了,这种变化不会影响方法的正确性。某些实施例直接在运行时,就将锁定的对象移到了一个专门的起始对象集合中了,则本步骤就无须再扫描全体对象来确定遍历的起始对象了,可以直接使用该起始对象集合。
只要有“灰”的对象,系统就可以开始调用该对象的遍历函数,完成该对象的跟踪遍历操作,不一定要等所有的起始对象被确定。各种不同的遍历方法都可以采用,它可以是深度遍历、广度遍历、或者任意的穷尽(Exhaustive)的遍历方法。当一个对象的所有引用的对象都变成了“灰”色,则该对象就可以变成“黑”色。遍历的方法必须是穷尽的,即当没有并发的引用关系图的修改发生时,遍历必须正确地将“黑”、“白”对象区分出来,保证不会有活动的对象没有被遍历到,而导致错误标记为“白”色的垃圾对象。
与此同时,并发的引用关系图修改将导致“白”色的对象转成“灰”色,系统必须同样处理这些“灰”色对象,执行遍历函数,将它们转成“黑”色。系统需要不断地执行步骤1202直到没有“灰”色的对象为止。系统的遍历工作总是能赶上应用程序对引用关系的修改的,这是本方法的特性所决定的。因为随着系统的遍历工作的进行,“灰”色的对象不断地变成“黑”色,应用程序则不断将“白”色转成“灰”色,而方法保证了“白”色对象的个数不会增加,所以总会有一个时刻,全部的“灰”色对象都转成了“黑”色,系统的遍历赶上了应用程序对引用关系的修改。
一旦“灰”色的对象都转成了“黑”色,则不会再有新的“灰”色对象产生。原因如下,步骤1202保证了穷尽的遍历和捕获操作期间的对引用关系的修改。采用反证法,首先如果假设在步骤1203的时候,已经没有了“灰”色对象,但是存在一个隐藏的有效引用指向“白”色对象,而且该引用在步骤1203之前就已经成功建立好了。那么有在图12B中,对象分成两类,“白”色和“黑”(及“灰”)色。其中,对象L 1211是循环引用的垃圾之一,对象H 1212和对象K 1213是“黑”色对象,一个未被检测到的引用1214指向对象L 1211。由于所有的有效指针都应该用智能指针替换(或者按智能指针相同的方法处理),那么实施引用1214的智能指针有两种可能,该指针的最后一次赋值操作发生在步骤1201之前,或者发生在其之后。
假设发生在步骤1201之后,由于该引用已经在步骤1203之前就建立好了,所以该最后一次的赋值操作必然被系统捕获,赋值操作的“右值”对象L 1211则会被检查,而从“白”转成了“灰”,此结果与假设发生矛盾。
如果最后一次赋值操作发生在步骤1201之前,则又有两种可能,如果智能指针是CLockedPtr类型,则对象L 1211将一直保持锁定(因为是最后一次赋值操作),所以系统能够正确将对象L 1211识别出来,作为“灰”色的跟踪遍历的起点;如果智能指针属于某个“黑”色的对象,如图12B中的对象H 1212所示,则当对象H 1212被转成“黑”色的时候,它的所有引用的对象必然已经转成了“灰”,这其中也包含了对象H 1211,结果与假设发生矛盾。至此,所有的可能性都进行了分析,都导出了与原假设相反的结果,所以原假设不成立。
既然,在步骤1204的时候,没有可能存在一个指向“白”色对象的引用,那么也就不可能在此之后又有指向“白”色对象的引用。因为,应用程序保证了赋值操作成功完成之后,原来的指针变量才可能改变或清空(错误的次序显然会令对象被提前回收),这意味着被引用的对象在原来的指针变量变化之前就会转成“灰”色。所以,如果假设有一个“白”色对象将要转成“灰”色,那么必然存在原来的指针变量还未改变或清空,该现存的指针变量根据上面的分析必然会被检测出来,假设不成立。
所以,一旦发生了“灰”色对象全部处理完了,就不会再有新的“灰”色对象出现。(在本次垃圾回收操作过程期间)。
图13是本实施例的垃圾回收操作的标记阶段的数据框图。
在图13中,有三个函数代码在执行,它们是赋值操作1301、创建对象1302和回收器1303。前两个函数由应用程序进行调用,后一个则由系统执行。
排他锁L1和标志F1 1304用于保护共享数据,共享数据是指被系统线程和应用程序线程的共同访问的数据,即可能被赋值操作1301和创建对象1302操作访问到的回收器的内部数据。集合SA 1305和SG 1306将赋值操作1301与回收器1303分隔开来,令两者在大部分时间里都可以并发地工作。这两个集合存放着应该变成“灰”色的“白”色对象的引用。赋值操作1301只访问SA 1305,而回收器则只访问SG 1306并在适当的时候交换SG 1306和SA 1305。在标记阶段期间,创建对象1302操作只访问集合SB 1307;在其他时间, 它可以直接访问“黑”色对象的列表LB 1309。回收器1303扫描“白”色对象列表LW 1308并将锁定的“白”对象转成“灰”色,移到“灰”对象列表LG 1310中,并跟踪遍历“灰”色对象将它们转成“黑”色,移入LB 1309中。
使用这样的数据结构,引用跟踪遍历操作就可以有较小的同步开销,更加有效率。赋值操作1301和创建对象1302必须先获得排他锁L1 1304,才能访问集合SA 1305和SB 1307。回收器也必须先获得L1 1304才能交换SA 1305和SG 1306。
图14是本实施例的标记阶段的流程图,图15是赋值操作的流程图,图16是创建对象操作的流程图。
在图14中,步骤1401对应图12A中的步骤1201,它在锁定L1的情况下,完成标志F1的设置,以便赋值操作和创建对象的操作能够检测到当前回收器的状态;所有的对象被切换成“白”色,从LB 1309切换到LW 1308。
步骤1402及随后的1403、1404、1406等,都属于图12A中的步骤1202。步骤1405则属于步骤1203。首先,步骤1402扫描确定锁定的“白”色对象,移入“灰”色对象列表LG 1310。步骤1403跟踪遍历这些“灰”色对象,将它们全部转成“黑”色。在此期间,应用程序自由地运行不会发生阻塞,因为该操作只涉及内部的数据结构,例如LW1308、LB1309和LG1310,并不使用SA1305和SB1307。
在图15中,如果有赋值操作发生在此期间,而且该操作的“右值”是一个“白”色对象的引用,则执行步骤1501,将该“白”色对象的引用加到SA 1305中,并在L1 1304锁定的情况下进行。
在图16中,新创建的对象将作为“黑”色对象处理,当回收器正在运行的时候,新对象被加到SB 1307中,其他时间则直接加到LB 1309中。这些操作都在L1 1304的保护下进行。
系统不断地进行跟踪遍历的操作直到步骤1405检测到“灰”色对象为空,步骤1404锁定L1 1304并交换SA1305和SG1306,步骤1406则将SG1306中的对象进行跟踪遍历,改为“黑”色。当步骤1405检测到“灰”色对象没有了,则意味着标记工作的结束,该判断逻辑上是发生在步骤1404的锁定状态下的,因为从步骤1404到步骤1405其间,没有代码能访问SG 1306。步骤1407清除了标志F1,结束了标记阶段,“黑”色对象列表LB1309再次对应用程序的开放,创建新的对象可以直接加到该列表中。“白”色的不可达对象则确定为垃圾对象,将被回收。
步骤1408在L1 1304的保护下,将新创建的对象从SB 1307加回到LB 1309中,如果该操作不能很快完成,则可以逐个对象进行处理,以保证不会长时间地占据L1 1304排他锁。
全部步骤描述完毕,可以发现在这些步骤中,没有调用挂起线程的操作,而且所有独占排他锁L1 1304的时间都很短,并可以预见如果发生竞争将导致的最坏的情况。标记阶段的竞争步骤包括步骤1401、1404、1408、1501、1601、1602,这些竞争下的操作都是简单的、常量复杂度的。
如果采用Snapshot-at-beginning增量回收方法,可以以本实施例作为基础稍加改变完成。区别主要在于指针的赋值操作将导致对原来“左值”对象进行簿记,而不是新引用的“右值”对象。在步骤1501中,如果在标记操作期间,赋值操作中原来的“左”值对象是“白”色,则将该“白”色对象的引用在L11304的保护下加到SA 1305中。其他操作基本不变。
在标记期间,如果某对象的引用计数值降到0则可以按实施例一的方法进行处理,即建立一个队列将这些零引用的对象记录在案,然后在标记结束之后再进行回收。这样,零引用对象的回收不会发生在垃圾回收的过程中,也无须特别的同步机制进行保护,因此整个系统在发生竞争的最坏情况下,可能造成应用程序发生阻塞的长度就是上面描述的那些竞争步骤,例如1401、1404、1408等。而且,这些竞争是多线程之间的标准的竞争行为,没有使用暂停挂起线程的操作,也无须对所有的线程进行挂起操作,只是在两个线程发生竞争的情况下,才会有阻塞,所以整个系统对应用程序的运行影响很小。
另外一种处理零引用对象方法,则是在垃圾回收的过程中直接进行零引用对象的回收,而不是延迟到标记结束之后才进行,下面对此详细说明。
直接完成零引用对象的回收的好处是它提供了严格的析构函数的执行次序。一个零引用的对象的析构,可能导致其他的对象的引用计数降到0,从而递归地执行析构函数。这样,析构函数的完成次序就完全与常规的纯引用计数的回收方法一致了。换一个角度,在这样的实施例中,当一个对象的引用数为0的时候,系统对该对象采取的操作,不应该受到正在进行中的增量垃圾回收操作的影响。
要达到这样的效果,必须在零引用对象的回收操作与增量垃圾回收操作之间采取同步机制进行协调。回收零引用对象需要使用到回收器的内部数据结构,包括LB1309、LG1310、LW1308,所以回收器和零引用对象回收操作访问这些对象时,需要先锁定相关的锁。此外,当回收器调用对象的遍历函数的期间需要锁定,以防止并发地发生该对象的回收操作,因为对象的析构函数通常不能与对象遍历函数并发执行。
本实施例是这样实现的,除了L1 1304另外再定义一个排他锁,它保护的对象包括,LB1309、LG1310、LW1308及对象的遍历函数的调用。回收器在使用这些数据结构之前必须先获得该锁,调用对象的遍历函数也必须在获得该锁的情况下进行。当对象的引用计数为0时,系统首先获得该排他锁;然后检查回收器是否在某些特别的操作下,例如在扫描所有对象的过程中,如果是这种情况,则需要进行相关的一些处理,保证这些特别的操作在对象被删除之后仍能继续进行,例如判断对象扫描的当前指针是否正在删除的对象,是则该指针改为指向下一个;然后将回收器中各种可能引用该对象的地方都检查一遍,将这些引用去除,包括从LB1309、LG1310、LW1308列表中将该对象去除;最后,回收该对象。
总的来说,就是当一个对象的引用数为0时,代表应用程序不再使用该对象,但是回收器中可能还存在该对象的引用,必须去除,而且回收器还可能正在使用该对象,例如执行其遍历函数,必须等待回收器完成使用,而回收器应该尽可能将长的操作改为可中断的操作。在本实施例中,最长的操作就是执行对象的遍历函数,该函数由应用程序编写,因此最坏的竞争情况可以由应用程序确定。系统可以提供服务允许一个对象的遍历函数分成多个部分分别完成,以减小竞争的粒度,避免阻塞太长的时间。例如,遍历函数根据调用的参数,决定遍历部分;或者允许为一个对象定义多个遍历函数,每个只完成部分工作。
如果某个应用程序的函数不涉及内存分配的变化,即不创建对象,也不会减少对象的引用数(或者延迟零引用的回收),则该函数的执行过程受到垃圾回收的影响极小,即使发生竞争,导致的阻塞时间也与对象的遍历函数无关,是可以预计的常量。这是目前各种垃圾回收方法无法做到的,也是很有实际价值的,例如,某外部事件的处理函数,就通常可以满足上述的条件,即无须分配创建对象,释放对象。这些函数通常都需要较高的实时响应能力,即使没有硬的实时要求,如果能够尽快地响应也往往会带来整体效率的提高。
在这样的基础上,还可以作出各种改进。例如,可以基于这样的事实,在回收操作的过程中,对内部数据的访问次数和频率要远远高于零引用对象的回收,可以设计一种同步机制,令回收操作先锁定资源,然后在执行的过程中,频繁地检查应用程序是否提出竞争请求,是则释放该锁,令应用程序有机会运行。这种检查显然要比不断地获得随后又释放排他锁要快得多。同样的原理也可以应用于其他竞争处理方面,只要竞争双方对共享数据的访问频率的相差较大就可以收到较好的效果。
其他方面,对于本领域的技术人员来说,也可以很轻易地就作出各种修改和变化。
例1,可以将SA 1305、SG 1306和SB 1307去除,允许应用程序线程直接访问修改回收器的各种内部数据,当然这样会带来较大的同步开销,如果同步的粒度较小则回收器运行的效率会较低,相反如果同步的粒度较大,则回收器运行的效率提高的同时造成竞争时的阻塞时间较长。
例2,可以不用通过扫描来确定锁定的对象,而在运行时标识锁定的同时完成锁定的对象的集合管理,在垃圾回收操作时就直接使用该集合作为扫描的结果。
例3,某些应用环境需要单线程或者准单线程的对象模式,例如COM的STA(Single-ThreadedApartment),那么可以将垃圾回收操作作为消息处理函数完成,或者在另外一个线程中执行,但是将“白”对象的回收操作在消息处理函数中完成,不过在这种情况下对象的遍历函数仍需要注意是异步执行的。
例4,对象的附加控制块,图3A中的301,并不一定需要与用户自定义的数据结构紧邻,只要逻辑上关联即可,本文的实施例仅仅是示范而已,事实上,附加控制块可以是一指针,指出真正的有关控制信息,也可以是与用户自定义数据块地址相关联的关联数组的元素,或者其他方式。
例5,引用跟踪的遍历方法可以采用各种穷尽(Exhaustive)的遍历方法,例如在实施例二的增量引用遍历的过程中,可以在扫描对象以确定锁定的对象的过程中,插入跟踪遍历,不一定需要在扫描完成之后才开始跟踪遍历,跟踪遍历也不一定需要列表LG 1310,可以使用深度遍历的递归方法等。而三种颜色的增量原则为了逻辑上的表述方便而已,实际实现时不一定需要三种颜色,也可能需要更多的颜色来区分不同类型的对象。
例6,可以将引用计数部分去除,保留锁定计数器及增量跟踪回收,从而作为一种增量跟踪垃圾回收系统,该系统仍具备无暂停的实时系统的特性。实施中可以将引用计数器的维护工作去除,保留写屏障的赋值操作处理部分,并去除零引用对象回收的相关代码即可。
虽然本发明已以前述优选实例说明,然其并非用于限制本发明,任何本领域的普通技术人员,在不脱离本发明的精神和范围的情况下,可作各种的更动与修改。因此本发明的保护范围以所附权利要求为准。
权利要求
1.一种计算机内存的垃圾回收方法,其特征是应用程序在运行时自动标识出被“扩展根集”中指针所引用的对象,而不是描述根集中的指针或引用本身及其分布;系统在垃圾回收时则不必扫描根集,以确定其中的指针,而直接以应用程序动态标识的对象作为起点,展开引用跟踪回收。
2.根据权利要求1所述的垃圾回收方法,其特征在于使用引用计数的方式标识出被“扩展根集”指针所引用的对象,系统为每个接受本系统管理的对象(简称对象)维护与之相关的引用计数器,称为锁定计数器;锁定计数器的值反映代表了来自“扩展根集”的、指向该对象的引用数目,引用数目(即锁定计数器)非空则意味该对象被标识,进而作为系统在进行引用跟踪回收时的起始对象。
3.根据权利要求1所述的垃圾回收方法,其特征在于系统为每个受管理对象维护一个数据结构,该结构通过公知的集合管理手段,包括链表、数组、向量、聚集、集合、图、哈希表和及其组合,描述受管理对象被“扩展根集”中指针引用的状态,每个这样的结构应该作为集合的一个元素,而不是每个指针或者引用作为一个元素,并且在系统跟踪回收时,无须扫描根集以确定跟踪的起始对象。
4.根据权利要求2所述的垃圾回收方法,其特征在于运行的时候,系统维护一个起始对象集合,将锁定计数器的值为正数的对象加入到该集合中,在对象的锁定计数器的值为0时,将该对象从起始对象集合中剔除;在进行引用跟踪回收的时候,系统直接使用该起始对象集合中的对象作为引用跟踪的起点。
5.根据权利要求2或4所述的垃圾回收方法,其特征在于包括以下的步骤,步骤1,将引用某对象的指针分成若干类,其中一类是赋值和初始化过程不应造成“右值”对象的引用计数值发生变化的;步骤2,赋值的时候,将“左值”原来所指的对象的引用计数器自减,“左值”变量指向“右值”对象,“右值”变量清空,这里的清空是指该变量的值改为某预定的固定值或者动态可确定的数值集合元素之一;步骤3,指针初始化的时候与步骤2相同,区别在于因为“左值”为未初始化的无效值,不进行“左值”对象的引用计数器自减操作;步骤4,当指针超出作用域时,所指的对象的引用计数器进行自减操作。
6.根据权利要求5所述的垃圾回收方法,其特征在于除了锁定计数器外,还有引用计数器代表来自其他指针的引用个数;当锁定计数器和引用计数器的值均为零的时候,系统回收该对象。并且在适当的时候,可以通过引用跟踪回收方法将循环引用的垃圾对象回收。
7.根据权利要求2或4所述的垃圾回收方法,其特征在于采用增量的引用跟踪回收;应用程序在锁定计数器的维护操作的期间(或之前、之后的紧邻操作),实施写屏障及(或)多线程同步机制,与增量垃圾回收器进行并发多线程的协同工作。
8.根据权利要求7所述的垃圾回收方法,其特征在于增量的引用跟踪回收操作的过程中发生的赋值、初始化及等同操作,其相关对象如果是“白”色则移入“灰”色对象集合,等待跟踪遍历;新创建的对象则作为“黑”色处理;回收器将完成跟踪遍历之后的“灰”色对象移入“黑”色集合,并且在无须挂起所有相关的应用程序线程,或者阻塞所有相关的应用程序线程,以达到全局的引用关系图一致的情况下,通过判断是否存在“灰”色对象,作为“完成确定垃圾对象的工作”的标准。
9.一种计算机自动内存管理系统,其特征是应用程序在运行时自动标识“扩展根集”指针所引用的对象,在回收操作期间,如果“扩展根集”中的指针引用受管理对象的情况发生了实质性的变化,则立即引起回收器相关代码的执行,该代码将与回收器的跟踪遍历代码以多线程并发方式执行,回收器的跟踪遍历代码不依赖根集扫描来确定遍历的起点,不需要暂停应用程序线程来判断遍历的结束,系统维护对象的引用数目,当引用数为零时可以立即回收该对象,即使在引用跟踪的遍历过程中。
10.根据权利要求9所述的系统,其特征在于系统使用引用计数的方式标识被“扩展根集”指针所引用的对象,在引用跟踪回收过程中,当根集中的指针的内容发生变化的时候,引起系统将这些指针所涉及的“白”色对象进行簿记,在此后的跟踪回收时作为“灰”色对象处理;在跟踪回收期间新创建的对象则作为“黑”色处理;回收器将完成跟踪遍历之后的“灰”色对象移入“黑”色集合,当没有“灰”色对象等待遍历则完成垃圾对象的确认工作。
全文摘要
本发明涉及一种计算机自动内存管理技术,又称垃圾回收机制。该技术可以应用在常规的C++语言环境或其他环境下,提供精确的引用跟踪回收,支持隐藏的指针,以及对本地对象的跟踪;结合引用计数和引用跟踪的特点,提供确定性的对象析构,统一了资源管理和对象管理,令资源和内存在失去最后一个引用的同时立即得到释放;提供彻底无暂停的增量垃圾回收,可预测最坏的情况,适用于实时响应的操作系统内核;提供高效率的内存管理,仅仅只是循环引用的垃圾对象浪费了内存,精心设计的应用程序可以完全不需要垃圾回收;运行时的管理开销远低于常规的引用计数方法。
文档编号G06F9/50GK101046755SQ200610034590
公开日2007年10月3日 申请日期2006年3月28日 优先权日2006年3月28日
发明者郭明南 申请人:郭明南
网友询问留言 已有0条留言
  • 还没有人留言评论。精彩留言会获得点赞!
1