MDN wants to learn about developers like you: https://qsurvey.mozilla.com/s3/d6d7ff2e2f9c

这篇翻译不完整。请帮忙从英语翻译这篇文章

本文是对于在Firefox 3的XPCOM中引入的cycle collector的一个简要描述,描述了将一个已有的C++类修改为一个XPCOM cycle collection中的参与项的步骤。如果你认为你有关于类的循环引用导致的内存泄漏,那可以看看这个。

本文面向Mozilla C++开发者。

cycle collector干了些什么

cycle collector大部分时间在记录可能导致循环引用的XPCOM对象指针。在collector的空闲阶段,nsAutoRefCnt的变体通过collector快速的修改自己的注册状态(注册/注销),会传递出一个“可疑”的引用计数事件(从N+1到N,N≠0)。

collector会阶段性的唤醒并验证缓冲区中记录过的可疑指针。这是collector的扫描阶段。在这个阶段,collector会请求每个候选项的cycle-collection helper类,如果存在,则需要helper来描述候选项拥有的子结构。collector可以以这种方式建立一个从可疑对象开始的拥有权关系图。

如果collector发现一组对象出现了彼此互相引用,并且确定组内对象的引用计数全由组内其他对象提供,则认定这组对象为循环引用,并尝试释放它们。这是collector的释放阶段。在这个阶段,collector遍历已找到的循环引用对象,再次请求它们的helper对象,释放其子结构和其他对象的引用。

collector也能遍历JS堆,并定位传入和传出的循环引用。

collector失效

cycle collector是一个保守的设备。有些情况下,它无法回收循环引用。

  1. 它默认不怀疑任何指针;对象必须要标记它们自己为可疑对象,一般用nsCycleCollectingAutoRefCnt而不是nsAutoRefCnt。
  2. 它只遍历那些在QI(QueryInterface)时返回helper对象的对象。如果在遍历图的过程中遇到了未知的边,那它会直接放弃这条边,因此每条边都需要被标记为可疑对象,否则找不到环。
  3. helper对象中的TraverseUnlink方法不是魔法,是程序员编码的,如果代码写错了,那collector也还是会崩。
  4. collector不知道怎么去搜索一个存在栈中的具有所有权的临时指针,所以将它放在程序的最顶层运行是有必要的。虽然有额外的所有权指针时不会崩,但是它会无法统计已记录的对象的引用计数,这也有可能导致回收失败。

怎么标记一个类为候选项

cycle collector和你的类之间的接口使用xpcom/base/nsCycleCollector.h中的内容实现直接获取,但是在xpcom/glue/nsCycleCollectionParticipant.h中提供了很多方便的宏用来标记你的类。通常,如果你用nsCOMPtr的mBar和mBaz来修改类nsFoo,可以简化为几个简单的修改:

  1. 在nsFoo.h和nsFoo.cpp中包含头文件nsCycleCollectionParticipant.h。 
  2. 在nsFoo.cpp中加一行用于声明nsFoo类是cycle collection的候选项的语句:
    NS_IMPL_CYCLE_COLLECTION_CLASS(nsFoo)
  3. 在nsFoo的定义里,将写有NS_DECL_ISUPPORTS的行修改为NS_DECL_CYCLE_COLLECTING_ISUPPORTS.
  4. 在nsFoo的定义里public部分加一行NS_DECL_CYCLE_COLLECTION_CLASS(nsFoo)。如果nsFoo从多个接口继承而来的话,可以写成NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsFoo, nsIBar),在你QueryInterface nsFoo为nsISSupports时,nsIBar作为接口返回。(我们把nsIBar称为nsFoo的典型ISupport类型。)

  5. 在nsFoo.cpp里接口map处加一行NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(nsFoo) :
    NS_INTERFACE_TABLE_HEAD(nsFoo)
      NS_INTERFACE_TABLE2(nsFoo, 
                          nsIBar, 
                          nsIBaz)
      NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsFoo)
    NS_INTERFACE_MAP_END
    
  6. 在nsFoo.cpp里把NS_IMPL_ADDREF(nsFoo)修改为NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFoo),同样的修改NS_IMPL_RELEASE(nsFoo)NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFoo)。
  7. 可以在nsFoo.cpp中添加合适的NS_IMPL_CYCLE_COLLECTION_#宏, #是你类中的成员数量。如果nsFoo包含两个成员变量,mBar和mBaz,我们可以写成NS_IMPL_CYCLE_COLLECTION_2(nsFoo, mBar, mBaz)。

你的类可能会比这张图的结构更复杂。例如,你的类可能有好几个nsISupport基类,需要使用一些执行消除歧义的* _AMBIGUOUS宏。或者说你的类拥有复杂的所有权结构,这样简单的NS_IMPL_CYCLE_COLLECTION_N宏就低效了;你可能需要手动的实现helper类的Traverse和Unlink方法。即使是这样的情况,也可以使用NS_IMPL_CYCLE_COLLECTION_TRAVERSE_ {BEGIN,END}和NS_IMPL_CYCLE_COLLECTION_UNLINK_ {BEGIN,END}。可以在一些复杂的类中看到实例,例如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.

手动的实现Traverse和Unlink方法

每个可能包含循环回收对象的域都需要被传递给cycle collector,以检查通过这些域的循环。

用于Traverse的宏主要是是NS_IMPL_CYCLE_COLLECTION_TRAVERSE:

  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSomeMember)

Unlink同理:

  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSomeMember)

这些宏应当处理各种情况,像指向nsISupports对象的指针或者非nsISupports对象的指针,或者这些指针的数组,当然这些指针都是有引用计数的、被collector记录的指针。

处理JSObject

如果你的类需要保存一个指向JSObject的指针,你需要告知cycle collector。像这样操作:

首先,它必须被保存在JS::Heap<JSObject *>域,假如你的类nsFoo有一个域mSomeObj:

private:
  ...
  JS::Heap<JSObject*> mSomeObj;
  ...

当你在JS对象指针中存了东西时,你需要用mozilla::HoldJSObjects来告诉GC遍历它并保持这个对象的存活:

...
mSomeObj = ... ;
mozilla::HoldJSObjects(this);
...

在Unlink方法(或者析构函数)里,需要将对象指针置为空:

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFoo)
  ...
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSomeMember)
  ...
  //如果你的类是wrapper cache: 
  //NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER

  tmp->mSomeObj = nullptr;
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

在析构函数里调用

mozilla::DropJSObjects(this);

你需要在Traverse方法列出cycle collector中记录的成员,也就是非JS对象:

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFoo)
  ...
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSomeMember)
  ...
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

最后你需要把JS对象加入Trace方法:

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

如果你的类是一个wrapper cache,可能生成的代码用了NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_#宏而不是NS_IMPL_CYCLE_COLLECTION_#. 这个宏不能定义Trace方法,因此不能列出JS对象;所以你需要向上面那样手动实现Trace和Unlink。

处理 JS::Value fields

 这里说过,一个JS::Value可能引用一个字符串或者对象并且被GC控制。于是我们需要告知cycle collector这种成员变量的存在。这与JSObject相似,但是使用的是NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK宏:

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsFoo)
  ...
  NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mSomeJSVal).
  ...
NS_IMPL_CYCLE_COLLECTION_TRACE_END

文档标签和贡献者

 此页面的贡献者: kylinmt
 最后编辑者: kylinmt,