coreBugZJ

此 blog 已弃。

垃圾回收与弱引用 (转)

在一个允许在堆上动态分配内存空间并且采取隐式内存释放的程序设计语言里,如何确保内存的正确释放不再是程序员的关注点,而由运行时环境来提供支持。无法被程序引用的在堆上已分配的内存空间成为垃圾(无用内存单元)。运行时环境要清除垃圾有两种方式:比较积极的方式,引用计数;与比较懒惰的方式,垃圾回收。


引用计数方式会为每个已分配内存单元设置计数器,当计数器减少到0的时候就意味着该单元无法再被引用,于是立即执行释放内存的动作。垃圾回收方式的基本思想是mark-and-sweep(标记-清除),每隔一段时间或者在堆空间不足的时候才进行一次垃圾回收,每次垃圾回收先将所有堆上分配的内存单元标记为“不可到达”,然后从一组根引用开始扫描,把所有从根引用出发可以达到的单元标记为“可以到达”;然后把标记为“不可到达”的内存单元回收到可用的堆空间中。


这两种清除垃圾的方式的特点很不一样。


其中,引用计数方式有四个主要问题:
1、如果分配的内存单元本身比较小,则用于计数的计数器所占的空间就会变得明显(significant),而垃圾回收方式只需要为每个分配的内存单元设置一个比特位的标记;
2、维护计数器的状态需要消耗时间。每当一个指针或者引用被赋值的时候,计数器的状态都要跟着改变。像LISP这样的语言,几乎所有操作都涉及改变指针(因为要操纵链表),计数器的状态维护会占据整个程序执行时间中明显的部分。这并不像MSDN Channel 9上Stephan T. Lavavej对C++ TR1中的介绍中所说的“shared_ptr没有垃圾回收所带来的额外时间消耗”,引用计数只不过是把这种消耗平均的分摊到了程序运行的整个过程中而已;
3、计数器相关的代码可能会分布在运行时系统,甚至用户代码的各处,不便于代码的维护;
4、当存在循环引用时,内存的正确释放会比较复杂。当然并不是不可解决,下文会再提到。


标记-清除式的垃圾回收则有另外的一些问题:
1、标记-清除会不定时的产生运算资源消耗的高峰(spike)。在没有进行垃圾回收的时候,程序可以运行得比较顺畅,但在执行垃圾回收的时候一般需要把整个程序停下来并执行标记-清除的过程,除非使用并行回收机制。标记的过程可能很长并且很消耗资源,如果是实时系统则一般无法承受这种消耗高峰而宁可使用引用计数方式将消耗分摊到程序的整个运行过程上。分代式的垃圾回收在一定程度上缓解了这个问题,但并没有根除消耗高峰的问题;
2、当你最需要垃圾回收器工作的时候,它的运行效果却最差。最需要进行垃圾回收的时候显然是堆上的内存已经快分配尽了的时候,但此时已分配的内存单元很多,需要使用大量时间来做标记,但实际能释放的内存单元却未必很多。
3、标记-清除算法有两种实现思路,一是“保守式”(conservative),二是“准确式”(exact)。保守式不需要知道内存的具体布局形式,会把栈上和全局区上所有“看起来像指针”的数值看作指针并纳入标记计算中;准确式则要求运行时系统清楚的了解内存布局形式,能够分辨哪些数据是指针哪些不是,并且只将指针纳入标记计算中。前者未必能保证内存的准确释放(但能够保证正在被引用的内存不被释放),后者则相对需要消耗更多的内存和更多的时间。
有名的Boehm GC就是保守式的代表。去年开源了的Adobe Virtual Machine 2(AVM2,又称Tamarin)中的MMgc也是保守式的。未换用Tamarin之前的SpiderMonkey则使用了准确式的垃圾回收。

 
=========================================================================================

 
当代许多程序设计语言的运行时系统都采用了隐式内存释放的设计,并出于各自的设计目标选用了不同的清除垃圾的方法。


在许多采用引用计数方式实现垃圾清除的系统中都有所谓的弱引用,例如说Squirrel,为的是解决循环引用的问题。让我们来看看Squirrel 2.2参考手册里的一段描述:


引用

Weak References

The weak references allows the programmers to create references to objects without influencing the lifetime of the object itself. In squirrel Weak references are first-class objects created through the built-in method obj.weakref(). All types except null implement the weakref() method; however in bools, integers and float the method simply returns the object itself(this because this types are always passed by value). When a weak references is assigned to a container (table slot, array, class or instance) is treated differently than other objects; When a container slot that hold a weak reference is fetched, it always returns the value pointed by the weak reference instead of the weak reference object. This allow the programmer to ignore the fact that the value handled is weak. When the object pointed by weak reference is destroyed, the weak reference is automatically set to null.


换句话说,如果我们预先知道可能出现循环引用状况,而其中一些引用并不需要那么“强”,那么我们就可以使用弱引用来消除循环引用的问题。在这个语境下,弱引用就是不会影响计数器状态的引用。这意味着即使我们拥有对某个对象的弱引用也不会阻止它被清除;也就是说,我们并不能知道手上的弱引用是否指向一个有效的对象。一般弱引用的实现都会保证当某个弱引用指向的不是有效对象时它会被设置为空值,例如null、nil、Nothing之类。


有人为Visual Basic 5/6也提出了弱引用的实现方法:Avoiding Circular References: WeakReference in VB-Classic。


微软的Component Object Model(COM)可以说是应用的最广泛的采用引用计数式垃圾清除的系统吧,但它的IUnknown.AddRef和IUnknown.Release方法没少给程序员带来头疼,而且也没提供解决统一的循环引用检测机制。即便如此现在还是有很多大型系统运行于其上,而且看起来好好的(摇头
 不,不都是好好的。想想IE的内存泄漏问题吧。在IE里用JScript想要造成内存泄漏还是挺简单的,不知道怎么做的话搜一下吧。^ ^

吉里吉里2也采用了引用计数方式的垃圾清除,但为了保证垃圾的清除,特别是引用的循环和程序结束时的资源释放,做了许多特别措施。这个另外在找时间写。


=========================================================================================

 
如上文所述,在引用计数环境中使用弱引用主要是为了解决循环引用带来的问题。而标记-清除方式的垃圾回收并不会受循环引用的影响:假如一组对象相互存在引用,而从根引用组出发已经无法达到它们之中的任何一个,则它们仍然会被回收。


但是许多采取标记-清除方式垃圾回收的环境也提供了弱引用。为什么呢?


首先想到的,弱引用肯定是为了解决问题而存在的;也就是说垃圾回收还是有问题,无法根除内存泄漏的问题。


在这种环境下的内存泄漏经常是人为失误造成的:无意的长时间持有了对已经不需要的对象的引用。这种引用经常存在于生命期特别长的对象中,例如一些全局对象中;Java和C#等语言虽然不允许在类之外定义全局数据,但类变量(而不是成员变量)或者诸如singleton等的特例的表现与C/C++中的全局变量并没有什么区别。

 
为了解决这样的问题,像Java和.NET Framework这样的平台也提供了弱引用机制。与引用计数环境一样,弱引用也不影响判定某个内存单元是否为垃圾的标记计算。

 
在Java中有三种“弱”引用:java.lang.ref.WeakReference<T>、java.lang.ref.SoftReference<T>、java.lang.ref.PhantomReference<T>。
 .NET Framework中也有System.WeakReference。


下面几篇文章对它们分别做了介绍:


Java theory and practice: Plugging memory leaks with weak references
Understanding Weak References
C# WeakReference Example


=========================================================================================

 
参考


Concepts of Programming Languages, Seventh Edition, Heap Management, Pages 301-305, by Robert W. Sebesta。

P.S. 上文把"reference counting"与"garbage collection"(GC)放在了同一层次来讨论,但GC的定义在许多资料中都不一样。我个人的看法是"reference counting"只是与"mark-and-sweep"一样,属于GC的一种策略;GC这个概念应该比reference counting和mark-and-sweep的层次更抽象才对。不过由于本文参考的资料里是按照"reference counting"和"garbage collection"来区分的,这里也就沿用了。

posted on 2012-03-17 15:26 coreBugZJ 阅读(625) 评论(0)  编辑 收藏 引用 所属分类: ProgrammingLanguage


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理