垃圾收集 - 通过强引用访问对象总是安全的吗?

时间:2012-11-20 07:22:04

标签: c# garbage-collection

我的理解是,当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 个答案:

答案 0 :(得分:4)

是的,如果您的ClassB通过ClassC引用objCStrongRef个实例,那么如果ClassB仍然存在,您无需担心{ {1}}随意蒸发。例外情况是你写一个终结器,即

the ClassC

中,不应尝试与~ClassB() { ...} 进行交谈;因为你不知道哪个对象首先被最终确定。如果objCStrongRef在最终确定时需要一些东西,它应该有一个单独的ClassC - 尽管实际上终结器方法非常少见,你不应该在没有充分理由的情况下添加它们(非托管句柄等)

重新定稿顺序:不,没有特定顺序,因为完全循环是可能的 - 并且它需要在任意地被打破。

答案 1 :(得分:2)

在普通的托管方法中,只要B处于活动状态,就可以依赖B引用的所有强引用的内容。 B中对C的引用对B的生命周期有益。

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

这虽然是学术性的。您的代码示例未实现终结器,因此您无需担心bizzarro终结器世界。

答案 2 :(得分:0)

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

  

此外,请注意您无法控制何时   Finalize方法将执行。 Finalize方法在垃圾时运行   收集发生,这可能发生在您的应用程序请求时   更多的记忆。此外,CLR不对订单做出任何保证   调用Finalize方法,所以你应该避免写一个   Finalize方法访问其类型定义的其他对象   最终方法;那些其他对象可能已经完成   已经。但是,访问值类型实例或完全可以   未定义Finalize方法的引用类型对象。你也是   在调用静态方法时需要小心,因为这些方法   可以在内部访问已经完成的对象,从而导致   静态方法的行为变得不可预测。

答案 3 :(得分:0)

如果没有可以到达对象的任何类型的引用路径,则无法检查该对象曾占用的内存空间,除非或直到GC运行并使该内存空间可用为了重用而创建一些使用它的新对象;到发生时,旧对象将不再存在。无论GC何时运行,对象在最后一次引用被破坏或无法访问时都会立即无法访问。

具有活动终结器的对象始终具有引用路径,因为称为“终结队列”的数据结构包含对每个此类对象的引用。此队列中的对象是GC处理的最后一件事;如果发现一个对象被队列引用但没有别的引用,则引用将存储在称为“可分离”队列的结构中,该队列列出了应该在第一次机会运行Finalize方法的对象。一旦GC循环完成,该列表将被视为强大的根参考,但终结器线程将开始从中拉出一些东西。通常情况下,一旦从列表中提取项目,就不会在任何地方引用它们,它们就会消失。

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

原子语义可能重要的唯一情况是当两个或多个WeakReference实例针对同一个对象时。在那种情况下,理论上可能一个线程在GC使具有相同目标的另一个Target无效的时刻访问WeakReference的{​​{1}}属性。我不认为这种情况实际上会出现;通过让多个共享同一目标的WeakReference个实例也共享其WeakReference,可以防止这种情况发生。在这种情况下,对目标的访问很快就会发生,以使对象保持活动状态,否则句柄将失效,从而有效地使所有GCHandle个实例无效。