循环引用——它们是什么以及如何解决?
19648 为本文评分:
5.0
循环引用——它们是什么以及如何解决?
匿名 2014年11月6日,星期四
在最近的博文 What the bleep is IDL doing? Garbage collection 中,Dain 讨论了 IDL 的垃圾收集机制,该机制在对象或指针(统称为堆变量)的引用计数降至零时执行。然而,在某些情况下,这似乎并不会发生。如果是这种情况,代码中可能包含循环引用。
什么是循环引用?
当一个堆变量包含对第二个堆变量的引用,而第二个堆变量又包含指回第一个堆变量的引用时,就会发生循环引用。例如,如果 A 是一个对象,并且在 A 的某处存在对 B 的引用,而在 B 内部又存在指回 A 的引用,这就构成了循环引用。
以下是一个简单的例子:
p1 = Ptr_New(/ALLOCATE_HEAP) p2 = Ptr_New(p1) *p1 = p2 help, /HEAP
Heap Variables: # Pointer: 2 # Object : 0
在这个例子中,每个指针都有两个引用。一个引用包含在我创建的变量(p1 和 p2)中。对第一个指针的第二个引用位于第二个指针内部,反之亦然。如果我丢弃我持有的引用(通过将变量设置为 !NULL),IDL 会将每个指针的引用计数减一。从我的角度来看,这些指针已经不存在了。然而,它们仍然相互引用,因此 IDL 的引用计数永远不会降到零,这意味着这些指针不会被垃圾收集。
p1 = !null p2 = !null help, /HEAP
Heap Variables: # Pointer: 2 # Object : 0
这种情况常见于父子关系中。父级会跟踪其子级,有时子级需要知道其父级是谁。
循环引用也可能是三角形的,或者循环可能通过许多对象和指针延伸。与这些更复杂的循环引用相关的问题可能难以调试。
旁注:
虽然我不再拥有引用这些指针的变量,但我并没有永久丢失它们。只要它们是有效指针并且我知道它们的堆标识符,我就可以使用带 /CAST 关键字的 PTR_VALID 函数来检索它们。
p1 = Ptr_Valid(1, /CAST) help, p1
P1 POINTER =
为什么循环引用会成为问题?
循环引用可能会带来一些问题,主要原因是造成不必要的内存占用。如果变量已超出作用域,但底层的指针或对象没有被清理,内存就会“泄漏”。过多的泄漏,尤其是对于大型对象,会减慢处理速度,并最终可能导致 IDL 挂起。
此外,如果我将 HELP, /HEAP 作为调试的一种形式,现在我必须在找到我要找的东西之前,先筛选这些“死亡”的堆变量。
如何解决循环引用?
手动清理
如果你确信永远不再需要某个堆变量,可以通过使用 OBJ_DESTROY 或 PTR_FREE 手动销毁它来管理内存。然而,说起来容易做起来难。销毁堆变量应谨慎进行。试图使用先前已被销毁的指针或对象的代码将因错误而停止。此外,在上面的指针示例中,如果我没有保留对第二个指针的引用,释放 "p1" 也会释放第二个指针。这是因为第二个指针的引用计数降到了零,它被垃圾收集了。隐式垃圾收集常常会导致意想不到的结果。
旁注: 对于列表和哈希表,隐式垃圾收集是期望的行为。例如,如果我有一个嵌套的哈希表(例如来自调用 JSON_PARSE),并且我手动销毁了根级哈希表,其内部的哈希表将超出作用域并被垃圾收集。这使我无需手动递归清理每个嵌套的哈希表。
使用弱引用
当我调用 p2 = Ptr_New(p1) 时,我的变量 p2 是对指针的强引用。此外,该指针包含对 p1 的强引用。IDL 将增加每个指向它的强引用所对应的堆变量的引用计数。如果我不希望第二个直接引用第一个指针,但第二个需要知道第一个的存在,我可以使用弱引用。
弱引用意味着使用堆标识符来代替对象或指针引用。可以使用 OBJ_VALID 或 PTR_VALID 上的 /GET_HEAP_IDENTIFIER 关键字来检索堆标识符,并且如上所述,可以使用 /CAST 关键字从标识符中检索对象/指针。
遵循严格的所有权规则
有时遵循严格的所有权规则有助于避免令人困惑的引用循环。例如,创建对象的对象可以对该对象的生命周期负责。父子关系是应遵守所有权的一个很好的用例。父级应包含对所有子级的强引用,并且父级最好知道子级是否以及何时应被销毁(即,如果在父级被销毁后子级对程序变得无关紧要,那么父级应在其 ::Cleanup 方法中手动销毁子级)。
父级应拥有子级,反之则不然(尽管如果你问我两岁的女儿,她可能会不同意这个说法!)。因此,如果子级出于任何原因需要关于父级的信息,它应该使用弱引用而不是强引用。
禁用引用计数(谨慎使用!)
在某些情况下,你可能希望关闭 IDL 的自动垃圾收集。你可以通过调用 HEAP_REFCOUNT 函数(此函数将返回堆变量的当前引用计数,对调试有用)并设置 /DISABLE 关键字来实现。
如果你没有为此函数提供参数,垃圾收集将在全局范围内被关闭。
我建议你谨慎使用此功能,因为如果垃圾收集被关闭,那么作为程序员,你将完全负责程序中创建的每个对象的生命周期,包括你可能没有立即意识到的对象,例如嵌套列表或哈希表中的对象。如果不定期清空,垃圾桶很快就会被填满。