垃圾回收机制中,引用计数法是如何维护所有对象引用的?

以前看JVM的时候总有个问题:讲到垃圾回收肯定会提到引用计数法,其优缺点都说了很多,但都没说要如何去实现它,我想问的是,引用计数法通过如何维护所有对象引用的?
关注者
1071
被浏览
44322
从楼主的问题看来,楼主似乎把GC的几个术语混为一谈了。在回答楼主问题之前先提一点:介绍自动内存管理的科普文章可能会提到引用计数(reference-counting)方式,但现在主流的JVM无一使用引用计数方式来实现Java对象的自动内存管理。

楼主提到的“扫描”与“整理”都与引用计数方式无关。楼主所说的“扫描”可能指的是“trace”,而“整理”指的肯定是“compaction”。这些都是基于可到达性来判断对象是否存活的tracing GC里会出现的概念,具体来说通常出现在mark-compact GC(“标记-整理GC”)中。

基于引用计数与基于trace这两大类别的自动内存管理方式最大的不同之处在于:前者只需要局部信息,而后者需要全局信息。

引用计数方式最基本的形态就是让每个被管理的对象与一个引用计数器关联在一起,该计数器记录着该对象当前被引用的次数,每当创建一个新的引用指向该对象时其计数器就加1,每当指向该对象的引用失效时计数器就减1。当该计数器的值降到0就认为对象死亡。每个计数器只记录了其对应对象的局部信息——被引用的次数,而没有(也不需要)一份全局的对象图的生死信息。由于只维护局部信息,所以不需要扫描全局对象图就可以识别并释放死对象;但也因为缺乏全局对象图信息,所以无法处理循环引用的状况。更高级的引用计数实现会引入“弱引用”的概念来打破某些已知的循环引用,但那是另一个话题了。
在实际实现中,引用计数存在什么地方是个有趣的话题。可以侵入式的存在对象内,例如CPython就把引用计数存在每个受自动内存管理的Python对象的对象头里(PyObject的ob_refcnt字段),或者COM的IUnknown::AddRef()/Release();也可以非侵入式的存在对象外面,例如C++11标准库里的std::shared_ptr
计数器的管理(自增/自减)可能由人工完成,例如老的Objective-C,或者是从C++里使用COM,等等;也可能是自动管理,例如CPython、使用“自动引用计数”(ARC)的Objective-CC++/CX的“hat”、前面提到的C++11的std::shared_ptr等等。如果能自动管理,那么必然有一套明确的规则说明何种情况下一个引用会被认为失效;以std::shared_ptr为例的话,其析构函数被调用(例如离开作用域时)或者其指向别的对象时,原本指向的对象的引用计数就会减1。

Tracing GC与引用计数正好相反,需要全局的对象图信息,从对象图的“根”(也就是必然活的引用)出发扫描出去,基于引用的可到达性来判断对象的生死。这使得对象的生死状态只能批量的被识别出来,然后批量释放死对象。Tracing GC不显式维护对象的引用计数,只在trace的时候才能回答“有”还是“没有”活引用指向某个对象

实际上,在内存充裕的前提下,tracing GC的整体开销比引用计数方式更低一些,所以吞吐量(throughput)高一些。因为引用计数方式通常需要统计冗余的局部信息,而tracing GC则可以通过全局信息一口气批量判断对象的生死;如果是带整理的tracing GC,则其内存分配通常也会更快。
不过tracing GC通常会比引用计数方式的延迟(latency)大一些,而且内存越紧张的时候tracing GC的效率反而越低,所以在内存不太充裕的地方使用引用计数仍然是个合理的选择(例如iOS5上的ARC)。
The Garbage Collection Handbook的第6章有对各种基本GC方式的详细对比,这边就不赘述了。

GC相关的书我在豆瓣上整理了一份书单,[Garbage Collection][垃圾回收][自动无用内存单元回收]相关读物
其中ガベージコレクションのアルゴリズムと実装这本书有对几种语言实现里的GC做源码剖析,值得一读。书是日文的,不过我有在推动国内出版社引进和翻译它。请期待它的中文版的面世。