淺談 Cycle Collection

什麼是 Cycle Collection ?

Gecko 專案中,常常會使用 smart pointer 來管理動態配置的記憶體的釋放。當記憶體的 reference count 為零時,就判定為不會再被使用。然而如果物件互相持有,形成了 reference cycle,也就是謀智台客這篇「說說 nsCOMPtr 這東西」提到的環狀參照,就會造成每個物件的 reference count 都無法歸零。
哎呀呀,此時即使它們不再被使用,這些物件還是會一直佔用記憶體。這樣的 garbage cycle 就成為記憶體洩漏( memory leak ),真是工程師的惡夢呢。
為了避免這種情形發生,軟體工程師只能選擇:
  1. 手動的在適當的時機打破 reference cycle
  2. 使用 weak pointer 來避免 cycle 咬住記憶體。( 如何用 weak pointer 解決 reference cycle 呢? 請參考謀智台客文章 [1] )
然而這些方式需要較多的人為判斷,在大型專案中很容易犯錯。尤其在 Gecko 專案,許多類別使用上很容易出現 reference cycle 的情形。像是 DOM objects 多半都需要特別注意,甚至有的設計上就是會形成 reference cycle,單單只是繼承該類別就可能有 cycle(例如:nsWrapperCache類別)。因此 Gecko 使用一個稱為「Cycle Collection」 的機制,自動回收不需要的 reference cycle。

如何解決 Reference Cycle ?

Cycle collector 藉由觀察 reference count 增減的情況, 收集可能已形成 garbage cycle 的物件。接著透過這些物件的 cycle collection helper class 得知它們用強指標指向哪些物件,並持續走訪( traverse )這些物件。最後會做出一個在 heap 中物件的存取關係圖( graph ),當發現有一群物件的 reference 只被彼此持有時,就認定其為 garbage cycle 。此時 cycle collector 會透過 helper class 打斷( unlink ) 彼此持有的狀態,讓 reference count 歸零,最後透過 reference counting 的機制來回收這些記憶體。

Cycle collector 不會走訪所有擁有強指標的物件,只會針對所謂的 cycle collection participant 進行檢查。如果想參與這個 cycle collection 過程的類別,就要宣告剛剛提到的 helper class 。實作的方式等等會聊到,先讓我們看看這個 CC participant 負責做什麼事吧。

它主要提供這三個函式:
Trace:告訴 cycle collector 該類別會直接持有哪些 JavaScript object 。
Traverse:告訴 cycle collector 所有要走訪的 C++ 強指標。
Unlink: 清除所有持有的 reference count 。 (包括 C++ 與 JavaScript objects )

這三個函式負責告訴 cycle collector 如何走訪與打斷環狀參照。Traverse 和 Unlink 是每個類別都要實作的。而 Trace 只有當類別直接持有 JavaScript object resource 才需要實作。 (例如:JS::Heap, JS::Value, jsval )

淺談 Cycle Collection

若物件因環狀參照而保有 reference count,需 unlink 才能釋放。

如何寫出可參與 Cycle Collection 的類別 ?

該怎麼寫才能正確釋放記憶體呢? Programmer 需要為可能形成 cycle 的類別宣告 cycle collection participant,並且實作所需要的函式,像是剛剛提到的 Traverse、Unlink 。
聽起來要寫很多東西,還好 Gecko 中有許多方便的巨集可以用。本文將介紹較常用的巨集,想再深入了解的朋友,請參考 nsCycleCollectionParticipant.h [2] 的檔案內容 。

首先,要在 header file 中選一個適合的巨集來宣告 CC participant helper class 。

// 如果不需要 Trace 函式,請寫。
NS_DECL_CYCLE_COLLECTION_CLASS(_class)

// 如果需要 Trace 函式,則寫
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(_class)

// 不需要 Trace , 繼承自 _base (_base class 要有 CC participant 實作)
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(_class, _base)

// 需要 Trace , 繼承自 _base (_base class 要有 CC participant 實作)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(_class, _base)

然後,在 source file 中需求實作 Trace, Traverse, Unlink 函式,這裡列出一些常用的寫法。

// 開始實作 Cycle Collection Participant
NS_IMPL_CYCLE_COLLECTION_CLASS(_class)

// 實作 Trace
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(_class, _base)
  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mSomeObj)
  NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mSomeJSVal)
NS_IMPL_CYCLE_COLLECTION_TRACE_END

// 實作 Traverse
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(_class, _base)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStrongPtr)  
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

// 實作 Unlink
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(_class, _base)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSomeMember)
  tmp->SomeCleanUpFunction();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

在 _BEGIN 與 _END 之間,除了可以寫巨集外,也可以寫一般的程式邏輯判斷與函式呼叫。如果想呼叫自己的 member function ,只要加上
tmp->
就可以。上面的範例
_class
繼承了父類別
_base
,如果不需要繼承,請使用沒有  _INHERITED 字尾的對應巨集。
Trace
Trace 函式中要記得回報所有的 JavaScript reference ,
JS::Heap
可利用 
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK
回報 ,  
JS::Value
與 
jsval
可利用 
NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK
 回報。
Traverse
實作 Traverse 有兩個重點。首先,要用 
NS_IMPL_CYCLE_COLLECTION_TRAVERSE
回報所有的強指標 。另外,如果該類別有直接持有 JavaScript reference 還要寫 
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
,表示該類別需要 trace JavaScript object。
Unlink
比較需要注意的是 Unlink 函式,它要做的事情是破除 reference cycle,這有時需要對自己類別有一定了解。較單純的情況只要將所有持有 reference count 的成員用 
NS_IMPL_CYCLE_COLLECTION_UNLINK
放掉,較複雜的情況 Unlink 函式內還會寫一些關閉服務、移除 observer、放掉 wrapper 等等的工作。另外,如果你的類別有呼叫
mozilla::HoldJSObjects(this)
,要記得在 Unlink 的時候呼叫
mozilla::DropJSObjects(this)
。HoldJSObjects 通常會在建構子中呼叫,它告知 cycle collector 與 garbage collector 自己持有 JavaScript objects,當物件不再需要持有 JS object 時,就須要呼叫 DropJSObjects。(註:garbage collector 的說明請參考先前的謀智台客文 [4] )

最後,還要記得在 source file 實作 interface query 函式並覆寫

AddRef()
Release()
。它們都有方便的巨集可幫忙完成。
// 實作 interface query
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(_class)
  NS_INTERFACE_MAP_ENTRY(nsISomeInterface)
  . . .
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

// 實作 reference count 函式
NS_IMPL_CYCLE_COLLECTING_ADDREF(_class)
NS_IMPL_CYCLE_COLLECTING_RELEASE_INHERITED(_class)

和之前的範例一樣,加上 _INHERITED 就可以直接繼承有 cycle collection 能力的父類別 。

// 透過繼承實作 interface query
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(_class)
NS_INTERFACE_MAP_END_INHERITING(_base)

// 透過繼承實作 reference count 函式
NS_IMPL_ADDREF_INHERITED(_class, _base)
NS_IMPL_RELEASE_INHERITED(_class, _base)

透過這樣子的寫法,只要小心實作三個主要的函式,cycle collector 就可以正確釋放 reference cycle 的記憶體了,是不是很方便呢?大家在 Gecko 專案中或許會注意到一些不同的寫法,有的 CC participant 甚至沒有手動實作 Traverse 和 Unlink ,這是因為  nsCycleCollectionParticipant.h 中提供了許多不同的巨集,在一些簡單的情況下可用更少的行數來幫你實作這些函式,但用法上都是很類似的。

一開始不知道從何下手的朋友,可以參考 DOMRequest 的寫法。由於 DOMRequest 有 JS::Heap 型態的成員變數,且它的父類別 DOMEventTargetHelper 是個 CC participant ,所以 DOMRequest.h 中使用下面的巨集宣告。

NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(DOMRequest, DOMEventTargetHelper)

實作的部份請參考 DOMRequest.cpp ,針對 

JS::Heap mResult
用巨集實作 Trace ,針對 
nsCOMPtr mError
用巨集實作 Traverse ,並在 Unlink 函式中釋放這兩個成員變數的 reference。下面列出這三個函式的實作範例給大家參考。
NS_IMPL_CYCLE_COLLECTION_CLASS(DOMRequest)

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DOMRequest,
                                               DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mResult)
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMRequest,
                                                  DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMRequest,
                                                DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mError)
  tmp->mResult = JSVAL_VOID;
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_UNLINK
會呼叫該變數的 
ImplCycleCollectionUnlink()
函式,以 nsCOMPtr 為例,該函式會將自己設為 null pointer 。如果變數沒有實作
ImplCycleCollectionUnlink()
,亦可直接透過賦值清掉 reference 來達到 unlink 的效果。

參考資料:

[1] 說說 nsCOMPtr 這東西 (謀智台客)

[2] nsCycleCollectionParticipant.h (Mozilla Cross-Reference)

[3] Interfacing with the XPCOM cycle collector (Mozilla Developer Network)

[4] 你丟我撿!神奇的 Firefox 內部記憶體回收機制 (謀智台客)

[5] Cycle Collection  (by Kyle Huey)