未分配给变量的类实例是否会过早收集垃圾?

时间:2014-02-13 09:58:43

标签: c# .net garbage-collection

(我甚至不知道我的问题是否有意义;这只是我不理解的东西,并且在我的脑海里旋转了一段时间)

考虑使用以下课程:

public class MyClass
{
    private int _myVar;

    public void DoSomething()
    {
        // ...Do something...

        _myVar = 1;

        System.Console.WriteLine("Inside");
    }
}

像这样使用这个类:

public class Test
{
    public static void Main()
    {
        // ...Some code...
        System.Console.WriteLine("Before");

        // No assignment to a variable.
        new MyClass().DoSomething();

        // ...Some other code...
        System.Console.WriteLine("After");
    }
}

Ideone

上面,我正在创建一个类的实例而不将其分配给变量。

我担心垃圾收集器可能会过早删除我的实例。

我对垃圾收集的天真理解是:

  

“只要没有引用指向它就删除对象。”

由于我创建了我的实例而没有将其分配给变量,因此这种情况是正确的。显然代码运行正确,所以我的asumption 似乎是假的。

有人可以告诉我我缺少的信息吗?

总结一下,我的问题是:

(为什么/为什么不这样做)是否可以安全地实例化一个类而不将它指向变量或return它?

即。是

new MyClass().DoSomething();

var c = new MyClass();
c.DoSomething();

从垃圾收集的角度来看是一样的吗?

4 个答案:

答案 0 :(得分:31)

有些安全。或者更确切地说,它就像你有一个在方法调用之后没有使用的变量一样安全。

当GC可以证明任何东西不再使用任何数据时,一个对象有资格进行垃圾收集(这与说它将立即被垃圾收集不同)。

如果方法不使用当前执行点以后的任何字段,即使实例方法正在执行,也会发生。这可能是相当令人惊讶的,但除非你有一个终结器,这通常不是问题,这些日子很少见。

当你使用调试器时,垃圾收集器很多对它将收集的内容更加保守。顺便说一下。

以下是这个“早期收集”的演示 - 很好,在这种情况下提前完成,因为它更容易演示,但我认为它证明了这一点:

using System;
using System.Threading;

class EarlyFinalizationDemo
{
    int x = Environment.TickCount;

    ~EarlyFinalizationDemo()
    {
        Test.Log("Finalizer called");
    }    

    public void SomeMethod()
    {
        Test.Log("Entered SomeMethod");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Thread.Sleep(1000);
        Test.Log("Collected once");
        Test.Log("Value of x: " + x);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Thread.Sleep(1000);
        Test.Log("Exiting SomeMethod");
    }

}

class Test
{
    static void Main()
    {
        var demo = new EarlyFinalizationDemo();
        demo.SomeMethod();
        Test.Log("SomeMethod finished");
        Thread.Sleep(1000);
        Test.Log("Main finished");
    }

    public static void Log(string message)
    {
        // Ensure all log entries are spaced out
        lock (typeof(Test))
        {
            Console.WriteLine("{0:HH:mm:ss.FFF}: {1}",
                              DateTime.Now, message);
            Thread.Sleep(50);
        }
    }
}

输出:

10:09:24.457: Entered SomeMethod
10:09:25.511: Collected once
10:09:25.562: Value of x: 73479281
10:09:25.616: Finalizer called
10:09:26.666: Exiting SomeMethod
10:09:26.717: SomeMethod finished
10:09:27.769: Main finished

注意打印x的值之后对象的最终确定方式(因为我们需要对象以便检索x)但之前 / em> SomeMethod完成。

答案 1 :(得分:14)

其他答案都很好,但我想强调一点。

这个问题基本归结为:什么时候垃圾收集器可以推断出某个给定的对象已经死了?并且答案是垃圾收集器有广泛的自由使用任何技术它选择确定一个物体何时死亡,这种宽广的自由度可以带来一些令人惊讶的结果。

所以让我们从:

开始
  

我对垃圾收集的天真理解是:“一旦没有引用指向它就删除一个对象。”

这种理解是错误的错误。假设我们有

class C { C c; public C() { this.c = this; } }

现在C的每个实例都有一个对它的引用存储在其自身中。如果仅在对它们的引用计数为零时回收对象,则永远不会清除循环引用的对象。

正确的理解是:

某些引用是“已知的根”。当集合发生时,已知的根被跟踪。也就是说,所有已知的根都是活着的,活着的东西所指的一切也是活着的,过渡性的。其他一切都已经死了,可以进行填海工程。

不收集需要完成的死对象。相反,它们在终结队列中保持活动,该队列是已知的根,直到它们的终结器运行,之后它们被标记为不再需要完成。未来的收藏将第二次将它们识别为死亡,并且它们将被收回。

很多东西都是已知的根源。例如,静态字段都是已知的根。局部变量可能是已知的根,但正如我们将在下面看到的,它们可以以令人惊讶的方式进行优化。临时值可能是已知的根。

  

我正在创建一个类的实例而不将其分配给变量。

这里的问题很好,但它基于一个不正确的假设,即一个局部变量始终是一个已知的根分配对局部变量的引用不一定使对象保持活动。垃圾收集器可以随意优化局部变量。

我们举个例子:

void M()
{
    var resource = OpenAFile();
    int handle = resource.GetHandle();
    UnmanagedCode.MessWithFile(handle);
}

假设resource是具有终结器的类的实例,并且终结器关闭该文件。 终结者可以在MessWithFile之前运行吗?是的! resource是一个局部变量,其整个生命周期为M这一事实无关紧要。运行时可以意识到此代码可以优化为:

void M()
{
    int handle;
    {
        var resource = OpenAFile();
        handle = resource.GetHandle();
    }
    UnmanagedCode.MessWithFile(handle);
}

现在resource在调用MessWithFile时已经死了。终结器在GetHandleMessWithFile之间运行不太可能,但 legal ,现在我们正在处理已关闭的文件。

此处的正确解决方案是在调用GC.KeepAlive之后在资源上使用MessWithFile

要回答您的问题,您的担忧基本上是“是已知根的参考的临时位置吗?”并且答案是通常是,同样需要注意的是,如果运行时可以确定引用从未被解除引用那么它被允许告诉GC引用的对象可能已经死了。

换句话说:你问是否

new MyClass().DoSomething();

var c = new MyClass();
c.DoSomething();
从GC的角度来看,

是相同的。是。在这两种情况下,GC都可以在确定它可以安全地执行 时杀死对象 ,无论是否为本地变量c的生命周期。< / p>

您问题的答案较短:信任垃圾收集器。它经过精心编写,可以做正确的事情。你需要担心GC做错事的唯一情况是我设置的场景,终结器的时间对于非托管代码调用的正确性非常重要。

答案 2 :(得分:6)

当然,GC对您来说是透明的,不会发生早期收集。所以我想你想知道实现细节:

实现方法的实现类似于带有附加this参数的静态方法。在您的情况下,this值存在于寄存器中,并像这样传递到DoSomething。 GC知道哪些寄存器包含实时引用,并将它们视为根。

只要DoSomething可能仍然使用this值,它就会保持活动状态。如果DoSomething从不使用实例状态,那么实际上可以在方法调用仍在其上运行时收集实例。这是不可观察的,因此是安全的。

答案 3 :(得分:4)

只要你谈论单线程环境,你就是安全的。如果你在DoSomething方法中开始一个新线程,那么有趣的事情才会开始发生,如果你的班级有一个终结者,就会发生更多的乐趣。这里要理解的关键是你和运行时/优化器/等之间的许多契约只在一个线程中有效。当您开始使用一种非主要面向多线程的语言在多个线程上进行编程时,这是带来灾难性后果的事情之一(是的,C#是其中一种语言)。

在您的情况下,您甚至使用this实例,这使得意外收集更不可能在仍然在该方法内;在任何情况下,合同都是在单个线程上,您无法观察优化和未优化代码之间的差异(除了内存使用,速度等,但那些是“免费午餐”)。