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

Uwe*_*eim 26 .net c# garbage-collection

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

考虑拥有以下课程:

public class MyClass
{
    private int _myVar;

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

        _myVar = 1;

        System.Console.WriteLine("Inside");
    }
}
Run Code Online (Sandbox Code Playgroud)

并使用这样的类:

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

(Ideone)

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

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

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

"一旦没有引用指向它就删除一个对象."

由于我创建我的实例而不将其分配给变量,因此这种情况是正确的.显然代码运行正确,所以我的假设似乎是错误的.

有人能告诉我我失踪的信息吗?

总结一下,我的问题是:

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

new MyClass().DoSomething();
Run Code Online (Sandbox Code Playgroud)

var c = new MyClass();
c.DoSomething();
Run Code Online (Sandbox Code Playgroud)

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

Jon*_*eet 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

注意在打印值之后如何最终确定对象x(因为我们需要对象以便检索x)但是 SomeMethod完成之前.

  • 我不确定你是否首先向我指出了这一点,但我现在发现了一个随机本地`myObject = null;`的代码,在方法返回之前以"效率"的名义返回并且备用有点轻笑. (2认同)
  • @AdamHouldsworth:啊,是的,那些空分配总是很有趣. (2认同)

Eri*_*ert 14

其他答案都很好,但我想强调几点.

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

那么让我们开始:

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

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

class C { C c; public C() { this.c = this; } }
Run Code Online (Sandbox Code Playgroud)

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

正确的理解是:

某些参考文献是"已知根".当收集发生时,跟踪已知的根.也就是说,所有已知的根都是活着的,活着的东西所指的一切也是活着的,过渡性的.其他一切都已经死了,并且可以进行填海工程.

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

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

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

这里你的问题是一个很好的问题,但它是基于一个不正确的假设,即局部变量总是一个已知的根. 分配对局部变量的引用不一定使对象保持活动状态.垃圾收集器可以随心所欲地优化局部变量.

我们举个例子:

void M()
{
    var resource = OpenAFile();
    int handle = resource.GetHandle();
    UnmanagedCode.MessWithFile(handle);
}
Run Code Online (Sandbox Code Playgroud)

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

void M()
{
    int handle;
    {
        var resource = OpenAFile();
        handle = resource.GetHandle();
    }
    UnmanagedCode.MessWithFile(handle);
}
Run Code Online (Sandbox Code Playgroud)

现在resource已经死了,MessWithFile被称为.终结者在和之间运行是不太可能但合法的,现在我们正在弄乱已经关闭的文件.GetHandleMessWithFile

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

要回到你的问题,你的担心基本上是"是一个已知根的参考的临时位置?" 并且答案通常是肯定的,同样需要注意的是,如果运行时可以确定引用永远不会被解引用,则允许它告诉GC引用的对象可能已经死亡.

换句话说:你问是否

new MyClass().DoSomething();
Run Code Online (Sandbox Code Playgroud)

var c = new MyClass();
c.DoSomething();
Run Code Online (Sandbox Code Playgroud)

从GC的角度来看是相同的.是.在这两种情况下,GC允许杀对象的那一刻,它决定了它可以安全地这样做,无论局部变量的生存期c.

您问题的答案较短:信任垃圾收集器.它经过精心编写,可以做正确的事情.您需要担心GC执行错误操作的唯一情况是我设置的方案,其中终结器的时序对于非托管代码调用的正确性非常重要.


usr*_*usr 6

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

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

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