在一個允許在堆上動態(tài)分配內(nèi)存空間并且采取隱式內(nèi)存釋放的程序設(shè)計(jì)語言里,如何確保內(nèi)存的正確釋放不再是程序員的關(guān)注點(diǎn),而由運(yùn)行時環(huán)境來提供支持。無法被程序引用的在堆上已分配的內(nèi)存空間成為垃圾(無用內(nèi)存單元)。運(yùn)行時環(huán)境要清除垃圾有兩種方式:比較積極的方式,引用計(jì)數(shù);與比較懶惰的方式,垃圾回收。
引用計(jì)數(shù)方式會為每個已分配內(nèi)存單元設(shè)置計(jì)數(shù)器,當(dāng)計(jì)數(shù)器減少到0的時候就意味著該單元無法再被引用,于是立即執(zhí)行釋放內(nèi)存的動作。垃圾回收方式的基本思想是mark-and-sweep(標(biāo)記-清除),每隔一段時間或者在堆空間不足的時候才進(jìn)行一次垃圾回收,每次垃圾回收先將所有堆上分配的內(nèi)存單元標(biāo)記為“不可到達(dá)”,然后從一組根引用開始掃描,把所有從根引用出發(fā)可以達(dá)到的單元標(biāo)記為“可以到達(dá)”;然后把標(biāo)記為“不可到達(dá)”的內(nèi)存單元回收到可用的堆空間中。
這兩種清除垃圾的方式的特點(diǎn)很不一樣。
其中,引用計(jì)數(shù)方式有四個主要問題:
1、如果分配的內(nèi)存單元本身比較小,則用于計(jì)數(shù)的計(jì)數(shù)器所占的空間就會變得明顯(significant),而垃圾回收方式只需要為每個分配的內(nèi)存單元設(shè)置一個比特位的標(biāo)記;
2、維護(hù)計(jì)數(shù)器的狀態(tài)需要消耗時間。每當(dāng)一個指針或者引用被賦值的時候,計(jì)數(shù)器的狀態(tài)都要跟著改變。像LISP這樣的語言,幾乎所有操作都涉及改變指針(因?yàn)橐倏v鏈表),計(jì)數(shù)器的狀態(tài)維護(hù)會占據(jù)整個程序執(zhí)行時間中明顯的部分。這并不像MSDN Channel 9上Stephan T. Lavavej對C++ TR1中的介紹中所說的“shared_ptr沒有垃圾回收所帶來的額外時間消耗”,引用計(jì)數(shù)只不過是把這種消耗平均的分?jǐn)偟搅顺绦蜻\(yùn)行的整個過程中而已;
3、計(jì)數(shù)器相關(guān)的代碼可能會分布在運(yùn)行時系統(tǒng),甚至用戶代碼的各處,不便于代碼的維護(hù);
4、當(dāng)存在循環(huán)引用時,內(nèi)存的正確釋放會比較復(fù)雜。當(dāng)然并不是不可解決,下文會再提到。
標(biāo)記-清除式的垃圾回收則有另外的一些問題:
1、標(biāo)記-清除會不定時的產(chǎn)生運(yùn)算資源消耗的高峰(spike)。在沒有進(jìn)行垃圾回收的時候,程序可以運(yùn)行得比較順暢,但在執(zhí)行垃圾回收的時候一般需要把整個程序停下來并執(zhí)行標(biāo)記-清除的過程,除非使用并行回收機(jī)制。標(biāo)記的過程可能很長并且很消耗資源,如果是實(shí)時系統(tǒng)則一般無法承受這種消耗高峰而寧可使用引用計(jì)數(shù)方式將消耗分?jǐn)偟匠绦虻恼麄€運(yùn)行過程上。分代式的垃圾回收在一定程度上緩解了這個問題,但并沒有根除消耗高峰的問題;
2、當(dāng)你最需要垃圾回收器工作的時候,它的運(yùn)行效果卻最差。最需要進(jìn)行垃圾回收的時候顯然是堆上的內(nèi)存已經(jīng)快分配盡了的時候,但此時已分配的內(nèi)存單元很多,需要使用大量時間來做標(biāo)記,但實(shí)際能釋放的內(nèi)存單元卻未必很多。
3、標(biāo)記-清除算法有兩種實(shí)現(xiàn)思路,一是“保守式”(conservative),二是“準(zhǔn)確式”(exact)。保守式不需要知道內(nèi)存的具體布局形式,會把棧上和全局區(qū)上所有“看起來像指針”的數(shù)值看作指針并納入標(biāo)記計(jì)算中;準(zhǔn)確式則要求運(yùn)行時系統(tǒng)清楚的了解內(nèi)存布局形式,能夠分辨哪些數(shù)據(jù)是指針哪些不是,并且只將指針納入標(biāo)記計(jì)算中。前者未必能保證內(nèi)存的準(zhǔn)確釋放(但能夠保證正在被引用的內(nèi)存不被釋放),后者則相對需要消耗更多的內(nèi)存和更多的時間。
有名的Boehm GC就是保守式的代表。去年開源了的Adobe Virtual Machine 2(AVM2,又稱Tamarin)中的MMgc也是保守式的。未換用Tamarin之前的SpiderMonkey則使用了準(zhǔn)確式的垃圾回收。
=========================================================================================
當(dāng)代許多程序設(shè)計(jì)語言的運(yùn)行時系統(tǒng)都采用了隱式內(nèi)存釋放的設(shè)計(jì),并出于各自的設(shè)計(jì)目標(biāo)選用了不同的清除垃圾的方法。
在許多采用引用計(jì)數(shù)方式實(shí)現(xiàn)垃圾清除的系統(tǒng)中都有所謂的弱引用,例如說Squirrel,為的是解決循環(huán)引用的問題。讓我們來看看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.
換句話說,如果我們預(yù)先知道可能出現(xiàn)循環(huán)引用狀況,而其中一些引用并不需要那么“強(qiáng)”,那么我們就可以使用弱引用來消除循環(huán)引用的問題。在這個語境下,弱引用就是不會影響計(jì)數(shù)器狀態(tài)的引用。這意味著即使我們擁有對某個對象的弱引用也不會阻止它被清除;也就是說,我們并不能知道手上的弱引用是否指向一個有效的對象。一般弱引用的實(shí)現(xiàn)都會保證當(dāng)某個弱引用指向的不是有效對象時它會被設(shè)置為空值,例如null、nil、Nothing之類。
有人為Visual Basic 5/6也提出了弱引用的實(shí)現(xiàn)方法:Avoiding Circular References: WeakReference in VB-Classic。
微軟的Component Object Model(COM)可以說是應(yīng)用的最廣泛的采用引用計(jì)數(shù)式垃圾清除的系統(tǒng)吧,但它的IUnknown.AddRef和IUnknown.Release方法沒少給程序員帶來頭疼,而且也沒提供解決統(tǒng)一的循環(huán)引用檢測機(jī)制。即便如此現(xiàn)在還是有很多大型系統(tǒng)運(yùn)行于其上,而且看起來好好的(搖頭
不,不都是好好的。想想IE的內(nèi)存泄漏問題吧。在IE里用JScript想要造成內(nèi)存泄漏還是挺簡單的,不知道怎么做的話搜一下吧。^ ^
吉里吉里2也采用了引用計(jì)數(shù)方式的垃圾清除,但為了保證垃圾的清除,特別是引用的循環(huán)和程序結(jié)束時的資源釋放,做了許多特別措施。這個另外在找時間寫。
=========================================================================================
如上文所述,在引用計(jì)數(shù)環(huán)境中使用弱引用主要是為了解決循環(huán)引用帶來的問題。而標(biāo)記-清除方式的垃圾回收并不會受循環(huán)引用的影響:假如一組對象相互存在引用,而從根引用組出發(fā)已經(jīng)無法達(dá)到它們之中的任何一個,則它們?nèi)匀粫换厥铡?/p>
但是許多采取標(biāo)記-清除方式垃圾回收的環(huán)境也提供了弱引用。為什么呢?
首先想到的,弱引用肯定是為了解決問題而存在的;也就是說垃圾回收還是有問題,無法根除內(nèi)存泄漏的問題。
在這種環(huán)境下的內(nèi)存泄漏經(jīng)常是人為失誤造成的:無意的長時間持有了對已經(jīng)不需要的對象的引用。這種引用經(jīng)常存在于生命期特別長的對象中,例如一些全局對象中;Java和C#等語言雖然不允許在類之外定義全局?jǐn)?shù)據(jù),但類變量(而不是成員變量)或者諸如singleton等的特例的表現(xiàn)與C/C++中的全局變量并沒有什么區(qū)別。
為了解決這樣的問題,像Java和.NET Framework這樣的平臺也提供了弱引用機(jī)制。與引用計(jì)數(shù)環(huán)境一樣,弱引用也不影響判定某個內(nèi)存單元是否為垃圾的標(biāo)記計(jì)算。
在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這個概念應(yīng)該比reference counting和mark-and-sweep的層次更抽象才對。不過由于本文參考的資料里是按照"reference counting"和"garbage collection"來區(qū)分的,這里也就沿用了。