簡介
最直觀的垃圾收集策略是引用計數。引用計數很簡單,但是需要
編譯器的重要配合,並且增加了
賦值函式 (mutator) 的開銷(這個術語是針對
用戶程式的,是從垃圾收集器的角度來看的)。每一個對象都有一個關聯的引用計數 —— 對該對象的活躍引用的數量。如果對象的引用計數是零,那么它就是垃圾(用戶程式不可到達它),並可以回收。每次修改
指針引用時(比如通過
賦值語句),或者當引用超出範圍時,編譯器必須生成代碼以更新引用的對象的引用計數。如果對象的引用計數變為零,那么運行時就可以立即收回這個塊(並且減少被回收的塊所引用的所有塊的引用計數),或者將它放到遲延收集佇列中。
com組件將維護一個稱作是引用計數的數值。當客戶從組件取得一個接口時,此引用計數值將增1。當客戶使用完某個接口後,組件的引用計數值將減1.當引用計數值為0時,組件即可將自己從記憶體中刪除。
引用計數的使用
原因
為什麼要選擇為每一個接口單獨維護一個引用計數而不是針對整個組件維護引用計數呢?
主要有兩個原因:一是使
程式調試更為方便;另外一個原因是支持資源的按需獲取。
1程式調試:
假設在程式中忘記對某個接口調用Release(其實很多人會犯這個錯)。這樣組件將永遠不會被刪除掉,因為只是在引用計數值0時delete才會被調用 。這時就需要找出接口在何時何處應該被釋放掉。當然找起來是相當困難的。在只對整個組件維護一個接口的情況下,進行這種 查找更為因難了。此時必須檢查使用了此組件所提供的所有接口的代碼。但若組件支持對每個接口分別維護一個引用計數那么可以把查找的範圍限制在某個特定的接口上。在某些情況下這可以節省大量時間。
2.資源的按需獲取
在實現某個接口時可能需要大量的記憶體或其他資源。對於此種情況,可以在
QueryInterface的實現中,在客戶請求此接口時完成資源的分配。但若只對整個組件維護一個引用計數,組件將無法決定何時可以安全地將此些接口相關聯的
記憶體釋放。但基對每個接口分別維護一個引用計數,那么決定何時可以將此記憶體釋放將會容易得多。
規則
正確使用引用計數三條簡單的規則
1. 在返回之前調用AddRef。對於那些建好些返回接口
指針的函式,在返回之前應該相應的指針調用AddRef。這些函式包括QueryInterface 及CreateInstance。這樣當客戶從這種 函式得到一個接口後。它將無需調用AddRef.
2.使用完接口之後調用
Release。在使用某個接口之後應該調用些接口的Release函式。
3.在賦值之後調用AddRef. 在將一個接口指針賦給另一個接口指針時,應調用AddRef。換句話說,在建立接口的別外一個引用之後應增加相應組件的引用計數。
接口
在客戶看來,引用計數是處於接口級的而不是組件級的。擔從實現的角度來看,誰的引用計數被記錄下來實際上沒有關係。客戶可以一直接相信組件將記錄每個接口本身維護引用計數值。但客戶不能假設整個組件維護單個的引用計數。
對於客戶而言,每一個接口被分別維護一個引用計數意味著客戶應該對它將要使用的
指針調用AddRef,而不是其他的什麼指針。對於使用完了指針客戶應該調用其
Release。
選擇為每一個接口單獨維護一個引用計數而不是針對整個組件維護引用計數的原因:
調試
可以通過增大和減少某個數的值而實現之。
另外要注意的是AddRef和
Release的返回值沒有什麼意義,只是在
程式調試中才可能會用得上.客戶不應將此從此值當成是組件或其接口的精確引用數。
規則
客戶必須對每一個接口具有一個單獨的引用計數值那樣來處理各接口。因此,客戶必須對不同的接口分別進行引用計數,即使它們的
生命期是
嵌套的。
一、輸出參數規則
輸出參數指的是給函式的調用者傳回一個值的函式參數。從這一點上講,輸出參數的作用同函式的
返回值是類似的。任何在輸出參數中或作為返回值返回一個新的接口
指針的函式必須對些接口指針調用AddRer。
二、輸入參數規則
對傳入函式的接口指針,無需調用AddRef和
Release,這是因為函式的生命期嵌套在調用者的生命期內。
三、輸入-輸出參數規則
輸入-輸出參數同時具有輸入參數及輸出參數的功能。在函式休中可以使用輸入-輸出參數的值,然後可以對這些值進行修改並將其返回給調用者。
在函式中,對於用輸入-輸出
參數傳遞進來的接口指針,必須在給它賦另外一個接口指針值之前調用其Release。在函式返回之前,還必須對輸出參數中所保存的接口
指針調用AddRef。
對於局部自製的接口
指針,由於它們只是在函式的生命其內才存在,因此無需調用AddRef和Release。這條規則實際是輸入參數規則的直接結果。在下面的例子中,pIX2隻是在函式foo的
生命期內都在,因此可以保證其生命期將嵌套在所傳入的pIX指針的生命期,因此無需對pIX2調用AddRef和
Release。
對於保存在全局變數中的接口
指針,在將其傳遞給另外一個函式之前,必須調用其AddRef。由於此變數是全局性的,因此任何函式都可以通過調用其Release來終止其
生命期。對於保存在
成員變數中的接口指針,也應按此種方式進行處理。因為類中的任何成員函式都可以改變此種接口指針的狀態。
六、不能確定時的規則
另外,在決定要進行最佳化時,應給那些沒有進行引用計數的
指針加上相應的注釋,否則其它程式設計師在修改代碼時,將可能會增大接口指針的生命期,從而合引用計數的最佳化遭到破壞。
忘記調用Release造成的錯誤可能比不調用AddRef造成的錯誤更難檢測。
優勢與缺陷
與跟蹤式垃圾回收相比,引用計數的主要優點是可以儘快地回收不再被使用的對象,同時在回收過程中不會導致長時間的停頓,還可以清晰地標明每一個對象的生存周期。
在
實時套用或記憶體受限的系統中,實時回響能力是一項重要指標,而引用計數作為最容易實現的垃圾回收技術之一,很適合於這種情況。引用計數還可以用於管理其他非記憶體資源,如
作業系統對象(經常比記憶體資源更稀缺)。跟蹤式垃圾回收技術用
終結器處理此類目標,但延遲回收可能引發其他問題。加權引用計數是適用於分散式系統的派生技術。
在可用記憶體被活躍對象填滿的平台上,跟蹤式垃圾回收會被頻繁觸發,從而降低性能。而引用計數即便在記憶體瀕臨耗盡的情況下性能依然有所保障。引用計數還能為其他運行時最佳化技術提供參考信息,例如對於許多使用
不可變對象的系統來說(如函式式程式語言),大量複製對象導致的性能懲罰有時十分嚴重;在此類系統上一個典型的最佳化措施是:假如一個對象被創建以後僅使用了一次,且在其不再被引用的同時另一個類似的對象被創建出來(如
Javascript中的字元串拼接賦值操作),可以將刪除原對象創建新對象的行為變為修改原對象,從而提高效率。引用計數可以為這類最佳化提供充分的參考信息。
未經最佳化的引用計數相比跟蹤式垃圾回收有兩個主要缺點,都需要引入附加機制予以修復:
另外,如果使用空閒列表分配記憶體,那么引用計數的空間局域性非常差。僅使用引用計數無法通過移動對象來提高
CPU快取的性能,所以高性能的記憶體分配器都會同時實現一個跟蹤式垃圾回收器以提高性能。許多引用計數實現(比如PHP和Objective-C)的性能不佳都是因為沒有實現記憶體拷貝。