【问题标题】:Could a class instance that is not being assigned to a variable get garbage-collected too early?未分配给变量的类实例是否会过早地被垃圾收集?
【发布时间】:2014-02-13 09:58:42
【问题描述】:

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

考虑有以下类:

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)

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

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

我对垃圾回收的幼稚理解是:

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

由于我创建实例时没有将其分配给变量,因此该条件为真。显然代码运行正确,所以我的假设似乎是错误的。

谁能给我我缺少的信息?

总而言之,我的问题是:

(为什么/为什么不)实例化一个类而不将其分配给变量或returning 它是否安全?

即是

new MyClass().DoSomething();

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

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

【问题讨论】:

  • 回答你的总结,是的,完全一样。与直接new Something().DoSomething(); 相比,局部变量没有或多或少的保证。甚至局部变量也不能保证在声明它的整个代码块中都存在——唯一的保证是它在最后一次使用之前不会被释放。
  • 当您执行new MyClass().DoSomething(); 时,我认为第一部分new MyClass() 将导致对新实例的引用被放入(推送)到调用堆栈中。即使它不是 C# 代码中的变量,仍然存在对堆栈上实例的引用。只要该引用可能被使用(例如.DoSomething()),就禁止GC 收集实例。那只是我的幼稚理解;我不是专家。

标签: c# .net garbage-collection


【解决方案1】:

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

当 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)但是之前@ 987654325@ 完成。

【讨论】:

  • 我不确定是不是你一开始就向我指出了这一点,但我现在发现了在方法返回之前执行随机本地 myObject = null; 的代码“效率”,别笑了。
  • @AdamHouldsworth:啊,是的,那些空任务总是很有趣。
【解决方案2】:

其他答案都很好,但我想在这里强调几点。

问题本质上归结为:何时允许垃圾收集器推断给定对象已死?答案是垃圾收集器有广泛的使用范围来使用它所使用的任何技术选择确定一个物体何时死亡,而这种广泛的纬度可能会导致一些令人惊讶的结果。

让我们开始吧:

我对垃圾收集的幼稚理解是:“只要没有引用指向它,就删除一个对象。”

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

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 之间运行,但合法,现在我们正在处理一个已关闭的文件。

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

回到你的问题,你关心的基本上是“引用的临时位置是一个已知的根吗?”并且答案是通常是的,同样需要注意的是,如果运行时可以确定引用是从不取消引用,那么它可以告诉 GC 引用的对象可能已经死了。

换一种说法:你问是否

new MyClass().DoSomething();

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

从 GC 的角度来看是相同的。是的。在这两种情况下,GC 都可以确定它可以安全地这样做时杀死对象,无论局部变量 c 的生命周期如何。

您的问题的简短答案是:相信垃圾收集器。它经过精心编写,是为了做正确的事。唯一需要担心 GC 做错事的情况是像我列出的场景,其中终结器的时间对于非托管代码调用的正确性很重要。

【讨论】:

  • 非常感谢,埃里克。我很荣幸能从你、乔恩和其他“传奇人物”那里得到这些很好的答案。这有助于我提高技能。
  • @UweKeim:很高兴!乐于助人。
【解决方案3】:

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

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

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

【讨论】:

    【解决方案4】:

    只要您谈论的是单线程环境,您就是安全的。只有在 DoSomething 方法中启动一个新线程时,有趣的事情才会开始发生,如果您的类有一个终结器,则更有趣的事情会发生。这里要理解的关键是,您与运行时/优化器/等之间的许多合同仅在单个线程中有效。当您开始使用主要不是面向多线程的语言(是的,C# 是其中一种语言)在多线程上进行编程时,这是会导致灾难性后果的事情之一。

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

    【讨论】:

      猜你喜欢
      • 2020-11-25
      • 2011-08-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多