3

我的理解是,当 GC 从主图中找到不再可访问(通过强引用)的对象的子图时,它将收集它们并回收内存。我的问题是关于删除不可访问对象的顺序。这是否作为原子操作发生?是否所有不可访问的对象都一次完成,或者 GC 是否在应用程序仍在执行时一个接一个地完成每个对象?如果对象是一个一个完成的,是否遵循特定的顺序?

如果我有一个对象 A 持有对对象 B 的弱引用,那么很明显 A 在调用 B 的任何实例方法之前必须检查 B 是否仍然存在。现在假设 B 持有对另一个对象 C 的强引用。如果 B 还活着,我是否总是保证 C 也仍然活着?GC是否有可能将B和C都标记为收集,但C在B之前完成?

我的猜测是从 B 访问 C 总是安全的(因为这是一个强参考)。但我想确认这个假设,因为如果我错了,我可能会引入一个非常间歇性的难以追踪的错误。

public class ClassA
{
    private readonly WeakReference objBWeakRef;

    public ClassA(ClassB objB)
    {
        objBWeakRef = new WeakReference(objB);
    }

    public void DoSomething()
    {
        // The null check is required because objB 
        // may have been cleaned-up by the GC
        var objBStrongRef = (ClassB) objBWeakRef.Target;
        if (objBStrongRef != null)
        {
            objBStrongRef.DoSomething();
        }
    }
}

public class ClassB
{
    private readonly ClassC objCStrongRef;

    public ClassB(ClassC objC)
    {
        objCStrongRef = objC;
    }

    public void DoSomething()
    {
        // Do I also need to do some kind of checking here?
        // Is it possible that objC has been collected, but 
        // the GC has not yet gotten around to collecting objB?
        objCStrongRef.DoSomething();
    }
}

public class ClassC
{
    public void DoSomething()
    {
        // do something here... if object is still alive.
    }
}
4

4 回答 4

4

是的,如果你ClassB通过 引用了一个ClassC实例objCStrongRef,那么如果ClassB它还活着,你就不必担心the ClassC随机蒸发。例外情况是如果您编写终结器,即

~ClassB() { ...}

那里,你根本不应该尝试与之交谈;因为您不知道哪个对象首先完成。如果在终结时需要某些东西,它应该有一个单独的- 尽管实际上终结器方法非常罕见,你不应该在没有充分理由的情况下添加它们(非托管句柄等)objCStrongRef ClassC~ClassC()

重新确定顺序:不,没有特定的顺序,因为完整的循环是可能的 - 它需要在某个地方任意中断。

于 2012-11-20T07:29:37.450 回答
2

在正常的托管方法中,只要 B 还活着,您就可以依赖 B 使用强引用引用的所有内容。B 对 C 的引用对 B 的整个生命周期都有好处。

例外是在完成代码中。如果你实现了一个终结器(这应该被认为是一种不寻常的情况,而不是常态),那么所有的赌注都在终结器的调用链中。在 GC 注意到对象不再被活动对象引用后,终结器可能不会执行很长时间 - 具有终结器的对象往往比“普通”对象停留的时间更长,这是不使用终结器的另一个原因。在极端恐慌关闭的极端情况下,终结器可能根本不会执行。您不能假设终结器将在哪个线程上执行,或者终结器将执行的顺序。并且您应该避免引用对象中的任何引用变量,因为您不知道它们是否已经被终结。

不过,这都是学术性的。您的代码示例没有实现终结器,因此您无需担心 Bizzarro 终结器世界。

于 2012-11-20T07:29:23.273 回答
0

来自 Jeffrey Richter 的“CLR via C#”:

此外,请注意您无法控制 Finalize 方法何时执行。Finalize 方法在垃圾收集发生时运行,这可能在您的应用程序请求更多内存时发生。此外,CLR 不保证 Finalize 方法的调用顺序,因此您应该避免编写访问其他类型定义了 Finalize 方法的对象的 Finalize 方法;那些其他对象可能已经完成。但是,访问没有定义 Finalize 方法的值类型实例或引用类型对象是完全可以的。调用静态方法时还需要小心,因为这些方法可以在内部访问已完成的对象,从而导致静态方法的行为变得不可预测。

于 2012-11-20T07:29:12.610 回答
0

如果不存在可以到达对象的任何类型的引用路径,则将无法检查该对象曾经占用的内存空间,除非或直到 GC 运行并使该内存空间可用于重用,并且创建了一些使用它的新对象;到那时,旧对象将不再存在。无论 GC 何时运行,对象在对它的最后一个引用被销毁或变得不可访问时立即变得不可访问。

具有活动终结器的对象始终具有引用路径,因为称为“终结队列”的数据结构包含对每个此类对象的引用。这个队列中的对象是GC最后处理的东西;如果发现一个对象被队列引用,但没有其他引用,则引用将存储在一个名为“freachable”队列的结构中,该队列列出了Finalize应在第一时间运行其方法的对象。一旦一个 GC 周期完成,这个列表将被认为是一个强根引用,但终结器线程将开始从中提取东西。通常,一旦从列表中拉出项目,任何地方都不会引用它们,它们就会消失。

弱引用增加了另一个问题,因为即使存在对它们的弱引用,对象也被认为符合收集条件。将这些视为有效的最简单方法是,一旦确定了每个需要保留的对象,系统将通过并使其WeakReference目标不需要保留的每个对象无效。一旦每个此类WeakReference实例都无效,该对象将无法访问。

原子语义可能很重要的唯一情况是当两个或多个WeakReference实例针对同一个对象时。在这种情况下,理论上一个线程可能在 GC 使另一个具有相同目标的线程无效的确切时刻访问Targeta 的属性。我认为这种情况实际上不会出现;可以通过让多个共享相同目标的实例也共享它们的. 在这种情况下,要么对目标的访问很快就会发生以使对象保持活动状态,要么句柄将无效,从而有效地使所有持有对它的引用的实例无效。WeakReferenceWeakReferenceWeakReferenceGCHandleWeakReference

于 2013-01-29T23:13:52.103 回答