This is a quick overview of the cycle collector introduced into XPCOM for Firefox 3, including a description of the steps involved in modifying an existing C++ class to participate in XPCOM cycle collection. If you have a class that you think is involved in a cyclical-ownership leak, this page is for you.
The intended audience is Mozilla C++ developers.
What the cycle collector does
The cycle collector spends most of its time accumulating (and forgetting about) pointers to XPCOM objects that might be involved in garbage cycles. This is the idle stage of the collector's operation, in which special variants of nsAutoRefCnt
register and unregister themselves very rapidly with the collector, as they pass through a "suspicious" refcount event (from N+1 to N, for nonzero N).
Periodically the collector wakes up and examines any suspicious pointers that have been sitting in its buffer for a while. This is the scanning stage of the collector's operation. In this stage the collector repeatedly asks each candidate for a singleton cycle-collection helper class, and if that helper exists, the collector asks the helper to describe the candidate's (owned) children. This way the collector builds a picture of the ownership subgraph reachable from suspicious objects.
If the collector finds a group of objects that all refer back to one another, and establishes that the objects' reference counts are all accounted for by internal pointers within the group, it considers that group cyclical garbage, which it then attempts to free. This is the unlinking stage of the collectors operation. In this stage the collector walks through the garbage objects it has found, again consulting with their helper objects, asking the helper objects to "unlink" each object from its immediate children.
Note that the collector also knows how to walk through the JS heap, and can locate ownership cycles that pass in and out of it.
How the collector can fail
The cycle collector is a conservative device. There are situations in which it will fail to collect a garbage cycle.
- It does not suspect any pointers by default; objects must suspect themselves, typically by using an
nsCycleCollectingAutoRefCnt
rather than ansAutoRefCnt
. - It only traverses objects that return a helper object when QI'ed to
nsICycleCollectionParticipant
. If it encounters an unknown edge during its traversal, it gives up on that edge; this means that every edge involved in a cycle must be participating, otherwise the cycle will not be found. - The
Traverse
andUnlink
methods on the helper object are not magic; they are programmer-supplied and must be correct, or else the collector will fail. - The collector does not know how to find temporary owning pointers that exist on the stack, so it is important that it only run from near the top-loop of the program. It will not crash if there are extra owning pointers, but it will find itself unable to account for the reference counts it finds in the owned objects, so may fail to collect cycles.
How to make your classes participate
The interface between the cycle collector and your classes can be accessed directly using the contents of xpcom/base/nsCycleCollector.h
, but there are convenience macros for annotating your classes in xpcom/glue/nsCycleCollectionParticipant.h
that are much easier to use. In general, assuming you are modifying class nsFoo
with two nsCOMPtr
edges mBar
and mBaz
, the process can be distilled to a few simple modifications:
- Include the header
nsCycleCollectionParticipant.h
in bothnsFoo.h
andnsFoo.cpp
. - Add a line declaring that your class
nsFoo
participates in the cycle collection innsFoo.cpp
:NS_IMPL_CYCLE_COLLECTION_CLASS(nsFoo)
- Change the line
NS_DECL_ISUPPORTS
toNS_DECL_CYCLE_COLLECTING_ISUPPORTS
in the definition ofnsFoo
. -
Add a line
NS_DECL_CYCLE_COLLECTION_CLASS(nsFoo)
within the public portion of definition ofnsFoo
. OrNS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsFoo, nsIBar)
ifnsFoo
inherits from multiple interfaces, wherensIBar
is the interface which is returned when youQueryInterface nsFoo
tonsISupports
. (We callnsIBar
the canonicalISupports
type fornsFoo
.) - Add a line
NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(nsFoo)
to the interface map ofnsFoo
innsFoo.cpp
. Or if that doesn't work:NS_INTERFACE_TABLE_HEAD(nsFoo) NS_INTERFACE_TABLE2(nsFoo, nsIBar, nsIBaz) NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsFoo) NS_INTERFACE_MAP_END
- Change the line
NS_IMPL_ADDREF(nsFoo)
toNS_IMPL_CYCLE_COLLECTING_ADDREF(nsFoo)
innsFoo.cpp
, and similarly change the lineNS_IMPL_RELEASE(nsFoo)
toNS_IMPL_CYCLE_COLLECTING_RELEASE(nsFoo)
innsFoo.cpp
. - Add the appropriate
NS_IMPL_CYCLE_COLLECTION_#
macro, where#
is the number of member variables in your class. For instance, ifnsFoo
contains two member variables,mBar
andmBaz
, we'd addNS_IMPL_CYCLE_COLLECTION_2(nsFoo, mBar, mBaz)
innsFoo.cpp
.
It is possible that your class has more complicated structure than this picture. For example, your class may have multiple nsISupports
base classes, which requires the use of some *_AMBIGUOUS
macros that perform a disambiguating downcast. Or your class may have a complicated ownership structure, such that the simple NS_IMPL_CYCLE_COLLECTION_N
macros are insufficient; in this case you might need to implement the Traverse and Unlink methods of your helper class manually. It's helpful even in these cases to use the NS_IMPL_CYCLE_COLLECTION_TRAVERSE_{BEGIN,END}
and NS_IMPL_CYCLE_COLLECTION_UNLINK_{BEGIN,END}
macros. You can see an example of their use in some more complicated classes such as content/base/src/nsGenericElement.cpp
. If your class has tearoffs or is being aggregated by other classes it is important to make the tearoff classes or the outer classes participate in cycle collection too, not doing so could lead to the cycle collector trying to collect the objects too soon.
Manually implementing the Traverse and Unlink methods
Each field that may contain cycle collected objects needs to be passed to the cycle collector, so it can detect cycles that pass through those fields.
The main macro for Traverse is NS_IMPL_CYCLE_COLLECTION_TRAVERSE:
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSomeMember)
Unlink works similarly:
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSomeMember)
These macros should handle a variety of cases, such as reference counted pointers to cycle collected nsISupports or non-nsISupports objects, as well as arrays of these pointers.
Handling JSObjects fields
If your class has a field of type JSObject
(really, it should be JS::Heap<JSObject *>) you need to tell the cycle collector about it; using JS_AddNamedRoot in a class method and JS_RemoveObjectRoot in your destructor is not the correct approach. The approach you should take instead is as follows.
Suppose your class nsFoo has field mSomeObj
:
private: ... JS::Heap<JSObject*> mSomeObj; ...
When you have something in the JS object pointer, you need to use mozilla::HoldJSObjects
to tell the GC to trace it and keep the object alive:
...
mSomeObj = ... ;
mozilla::HoldJSObjects
(this);
...
In the Unlink method (or destructor) you need to set the object pointer to NULL
:
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFoo) ... NS_IMPL_CYCLE_COLLECTION_UNLINK(mSomeMember) ... //if your class is a wrapper cache: //NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER tmp->mSomeObj = nullptr; NS_IMPL_CYCLE_COLLECTION_UNLINK_END
In the destructor, you should call
mozilla::DropJSObjects
(this);
In your Traverse method you need to list members involved in the cycle collector, i-e no JS objects:
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFoo) ... NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSomeMember) ... NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
Finally, you need to manually add the JS object to the Trace method:
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsFoo) //if your class is a wrapper cache: //NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mSomeObj) NS_IMPL_CYCLE_COLLECTION_TRACE_END
Note that if your class is a wrapper cache then you likely have generate code that uses NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_#
macro instead of NS_IMPL_CYCLE_COLLECTION_#
. Unfortunately this macro defines the Trace method and so you can't list your JS object; hence, you need to also manually implmenet Trace and Unlink as above.
Handling JS::Value fields
Recall (or see here) that a JS::Value
may reference a string or object and is subject to GC. Hence, we need to tell the cycle collector about any such member variables. This is the same as for the JSObject
case, but using the NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK
macro:
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsFoo) ... NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mSomeJSVal). ... NS_IMPL_CYCLE_COLLECTION_TRACE_END