没有被分配给变量的类实例会不会过早地被垃圾收集?

时间:2021-05-05 23:53:46

(I don't even know whether my question makes sense at all; it is just something that I do not understand and is spinning in my head for some time)

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

Consider having the following class:

考虑参加以下课程:

public class MyClass
{
    private int _myVar;

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

        _myVar = 1;

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

And using this class like this:

像这样使用这个类:

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)

(Ideone)

Above, I'm creating an instance of a class without assigning it to a variable.

上面,我创建了一个类的实例,但没有将其赋值给一个变量。

I fear that the garbage collector could delete my instance too early.

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

My naive understanding of garbage collection is:

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

"Delete an object as soon as no references point to it."

“一旦没有引用指向一个对象,就立即删除它。”

Since I create my instance without assigning it to a variable, this condition would be true. Obviously the code runs correct, so my asumption seems to be false.

因为我创建了我的实例而不将其赋值给一个变量,所以这个条件是正确的。显然代码运行正确,所以我的假设似乎是错误的。

Can someone give me the information I am missing?

有人能告诉我我所缺少的信息吗?

To summarize, my question is:

总之,我的问题是:

(Why/why not) is it safe to instantiate a class without asigning it to a variable or returning it?

(为什么/为什么不)实例化类而不将其赋值给变量或返回它是否安全?

I.e. is

即是

new MyClass().DoSomething();

and

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

the same from a garbage collection point-of-view?

从垃圾收集的角度来看也是如此吗?

4 个解决方案

#1


31  

It's somewhat safe. Or rather, it's as safe as if you had a variable which isn't used after the method call anyway.

这是比较安全的。或者更确切地说,它是安全的,就像您有一个变量,在方法调用之后没有使用它。

An object is eligible for garbage collection (which isn't the same as saying it will be garbage collected immediately) when the GC can prove that nothing is going to use any of its data any more.

当GC可以证明不再使用它的任何数据时,对象就可以进行垃圾收集(这并不等同于说它将立即进行垃圾收集)。

This can occur even while an instance method is executing if the method isn't going to use any fields from the current execution point onwards. This can be quite surprising, but isn't normally an issue unless you have a finalizer, which is vanishingly rare these days.

即使在实例方法执行时,如果方法不会从当前执行点开始使用任何字段,也会发生这种情况。这可能相当令人吃惊,但通常不是一个问题,除非你有一个终结器,这在现在是非常罕见的。

When you're using the debugger, the garbage collector is much more conservative about what it will collect, by the way.

顺便说一句,当您使用调试器时,垃圾收集器对于它将收集的内容要保守得多。

Here's a demo of this "early collection" - well, early finalization in this case, as that's easier to demonstrate, but I think it proves the point clearly enough:

这是这个“早期收藏”的一个演示——在这个例子中,早期定稿,因为这更容易演示,但我认为它充分证明了这一点:

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);
        }
    }
}

Output:

输出:

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

Note how the object is finalized after the value of x has been printed (as we need the object in order to retrieve x) but before SomeMethod completes.

请注意,在输出x的值之后(为了检索x,我们需要对象),但是在SomeMethod完成之前,对象是如何完成的。

#2


14  

The other answers are all good but I want to emphasize a few points here.

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

The question essentially boils down to: when is the garbage collector allowed to deduce that a given object is dead? and the answer is the garbage collector has broad latitude to use any technique it chooses to determine when an object is dead, and this broad latitude can lead to some surprising results.

问题本质上可以归结为:何时允许垃圾收集器推断给定的对象是死的?答案是垃圾回收器有很大的*度来使用它选择的任何技术来确定一个对象何时死亡,这个宽泛的纬度可以导致一些令人惊讶的结果。

So let's start with:

所以让我们开始:

My naive understanding of garbage collection is: "Delete an object as soon as no references point to it."

我对垃圾收集的天真理解是:“在没有引用的情况下立即删除对象。”

This understanding is wrong wrong wrong. Suppose we have

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

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

Now every instance of C has a reference to it stored inside itself. If objects were only reclaimed when the reference count to them was zero then circularly referenced objects would never be cleaned up.

现在,C的每个实例都有一个对它本身的引用。如果仅在引用计数为0时回收对象,则循环引用对象永远不会被清理。

A correct understanding is:

正确的理解是:

Certain references are "known roots". When a collection happens the known roots are traced. That is, all known roots are alive, and everything that something alive refers to is also alive, transitively. Everything else is dead, and eligable for reclamation.

某些引用是“已知的根”。当收集发生时,跟踪已知的根。也就是说,所有已知的根都是活的,所有活着的东西也都是活的,是短暂的。其他的一切都是死的,可以被回收利用。

Dead objects that require finalization are not collected. Rather, they are kept alive on the finalization queue, which is a known root, until their finalizers run, after which they are marked as no longer requiring finalization. A future collection will identify them as dead a second time and they will be reclaimed.

不收集需要终结的死对象。相反,它们在终结队列(即已知的根)上保持活动状态,直到它们的终结器运行,之后它们被标记为不再需要终结。未来的收藏将第二次确认它们是死的,它们将被回收。

Lots of things are known roots. Static fields, for example, are all known roots. Local variables might be known roots, but as we'll see below, they can be optimized away in surprising ways. Temporary values might be known roots.

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

I'm creating an instance of a class without assigning it to a variable.

我正在创建一个类的实例,而不将其赋值给一个变量。

Your question here is a good one but it is based on an incorrect assumption, namely that a local variable is always a known root. Assigning a reference to a local variable does not necessarily keep an object alive. The garbage collector is allowed to optimize away local variables at its whim.

你的问题很好,但它基于一个错误的假设,即局部变量总是已知的根。给局部变量赋值不一定能使对象保持活动状态。垃圾收集器可以随意地优化本地变量。

Let's give an example:

让我们举个例子:

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

Suppose resource is an instance of a class that has a finalizer, and the finalizer closes the file. Can the finalizer run before MessWithFile? Yes! The fact that resource is a local variable with a lifetime of the entire body of M is irrelevant. The runtime can realize that this code could be optimized into:

假设资源是具有终结器的类的实例,终结器关闭文件。终结器可以在MessWithFile之前运行吗?是的!资源是一个局部变量,整个M的生命周期是不相关的。运行时可以意识到该代码可以优化为:

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

and now resource is dead by the time MessWithFile is called. It is unlikely but legal for the finalizer to run between GetHandle and MessWithFile, and now we're messing with a file that has been closed.

现在,在调用MessWithFile时,资源已经死亡。终结器在GetHandle和MessWithFile之间运行是不可能的,但是它是合法的,现在我们正在处理一个已经关闭的文件。

The correct solution here is to use GC.KeepAlive on the resource after the call to MessWithFile.

这里的正确解决方案是使用GC。在调用MessWithFile之后保持资源的活动。

To return to your question, your concern is basically "is the temporary location of a reference a known root?" and the answer is usually yes, with the caveat that again, if the runtime can determine that a reference is never dereferenced then it is allowed to tell the GC that the referenced object might be dead.

回到你的问题,你的问题基本上是“临时位置的一个引用一个已知根?”的答案通常是肯定的,但需要说明的是,再一次,如果运行时可以确定一个参考从未引用时则允许告诉GC引用的对象可能会死。

Put another way: you asked if

换句话说:你问我。

new MyClass().DoSomething();

and

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

are the same from the point of view of the GC. Yes. In both cases the GC is allowed to kill the object the moment that it determines it can do so safely, regardless of the lifetime of local variable c.

从GC的角度来看是一样的。是的。在这两种情况下,GC都被允许在它确定它可以安全的情况下杀死这个对象,而不考虑本地变量c的生命周期。

The shorter answer to your question is: trust the garbage collector. It has been carefully written to do the right thing. The only times you need to worry about the GC doing the wrong thing are scenarios like the one I laid out, where timing of finalizers is important for the correctness of unmanaged code calls.

简短的回答是:信任垃圾收集器。为了做正确的事,它被小心地写了出来。您只需要担心GC做了错误的事情,就像我所描述的场景一样,终结器的时间对非托管代码调用的正确性非常重要。

#3


6  

Of course, GC is transparent to you and no early collection can ever happen. So I guess you want to know the implementation details:

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

An instance method is implemented like a static method with an additional this parameter. In your case the this value lives in registers and is passed like that into DoSomething. The GC is aware what registers contain live references and will treat them as roots.

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

As long as DoSomething might still use the this value it stays live. If DoSomething never uses instance state then indeed the instance can be collected while a method call is still running on it. This is unobservable, therefore safe.

只要DoSomething仍然使用这个值,它就会一直存活。如果DoSomething从未使用实例状态,那么确实可以在方法调用仍在其上运行时收集实例。这是不可观测的,因此是安全的。

#4


4  

As long as you're talking about a single threaded environment, you're safe. Fun things only start to happen if you're starting a new thread inside the DoSomething method, and even more fun happens if your class has a finalizer. The key thing to understand here is that a lot of the contracts between you and the runtime / optimizer / etc. are valid only in a single thread. This is one of the things that has disastrous results when you start programming on multiple threads in a language that isn't primaririly multi-threading oriented (yes, C# is one of those languages).

只要您谈论的是单线程环境,您就安全了。只有当您在DoSomething方法中启动一个新线程时,才会发生有趣的事情;如果您的类有终结器,则会发生更有趣的事情。这里要理解的关键是,您与运行时/优化器等之间的许多契约只在一个线程中有效。当您开始在一种非原始的面向多线程的语言(是的,c#是这些语言之一)上对多个线程进行编程时,这是一件很糟糕的事情。

In your case, you're even using the this instance, which makes unexpected collection even less likely while still inside that method; in any case, the contract is that on a single thread, you can't observe the difference between the optimized and unoptimized code (apart from memory usage, speed, etc., but those are the "free lunch").

在你的例子中,你甚至在使用这个实例,这使得在这个方法中,意外收集的可能性更小;无论如何,契约是在一个线程上,您无法观察优化代码和未优化代码之间的区别(除了内存使用、速度等,但这些都是“免费午餐”)。

#1


31  

It's somewhat safe. Or rather, it's as safe as if you had a variable which isn't used after the method call anyway.

这是比较安全的。或者更确切地说,它是安全的,就像您有一个变量,在方法调用之后没有使用它。

An object is eligible for garbage collection (which isn't the same as saying it will be garbage collected immediately) when the GC can prove that nothing is going to use any of its data any more.

当GC可以证明不再使用它的任何数据时,对象就可以进行垃圾收集(这并不等同于说它将立即进行垃圾收集)。

This can occur even while an instance method is executing if the method isn't going to use any fields from the current execution point onwards. This can be quite surprising, but isn't normally an issue unless you have a finalizer, which is vanishingly rare these days.

即使在实例方法执行时,如果方法不会从当前执行点开始使用任何字段,也会发生这种情况。这可能相当令人吃惊,但通常不是一个问题,除非你有一个终结器,这在现在是非常罕见的。

When you're using the debugger, the garbage collector is much more conservative about what it will collect, by the way.

顺便说一句,当您使用调试器时,垃圾收集器对于它将收集的内容要保守得多。

Here's a demo of this "early collection" - well, early finalization in this case, as that's easier to demonstrate, but I think it proves the point clearly enough:

这是这个“早期收藏”的一个演示——在这个例子中,早期定稿,因为这更容易演示,但我认为它充分证明了这一点:

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);
        }
    }
}

Output:

输出:

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

Note how the object is finalized after the value of x has been printed (as we need the object in order to retrieve x) but before SomeMethod completes.

请注意,在输出x的值之后(为了检索x,我们需要对象),但是在SomeMethod完成之前,对象是如何完成的。

#2


14  

The other answers are all good but I want to emphasize a few points here.

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

The question essentially boils down to: when is the garbage collector allowed to deduce that a given object is dead? and the answer is the garbage collector has broad latitude to use any technique it chooses to determine when an object is dead, and this broad latitude can lead to some surprising results.

问题本质上可以归结为:何时允许垃圾收集器推断给定的对象是死的?答案是垃圾回收器有很大的*度来使用它选择的任何技术来确定一个对象何时死亡,这个宽泛的纬度可以导致一些令人惊讶的结果。

So let's start with:

所以让我们开始:

My naive understanding of garbage collection is: "Delete an object as soon as no references point to it."

我对垃圾收集的天真理解是:“在没有引用的情况下立即删除对象。”

This understanding is wrong wrong wrong. Suppose we have

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

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

Now every instance of C has a reference to it stored inside itself. If objects were only reclaimed when the reference count to them was zero then circularly referenced objects would never be cleaned up.

现在,C的每个实例都有一个对它本身的引用。如果仅在引用计数为0时回收对象,则循环引用对象永远不会被清理。

A correct understanding is:

正确的理解是:

Certain references are "known roots". When a collection happens the known roots are traced. That is, all known roots are alive, and everything that something alive refers to is also alive, transitively. Everything else is dead, and eligable for reclamation.

某些引用是“已知的根”。当收集发生时,跟踪已知的根。也就是说,所有已知的根都是活的,所有活着的东西也都是活的,是短暂的。其他的一切都是死的,可以被回收利用。

Dead objects that require finalization are not collected. Rather, they are kept alive on the finalization queue, which is a known root, until their finalizers run, after which they are marked as no longer requiring finalization. A future collection will identify them as dead a second time and they will be reclaimed.

不收集需要终结的死对象。相反,它们在终结队列(即已知的根)上保持活动状态,直到它们的终结器运行,之后它们被标记为不再需要终结。未来的收藏将第二次确认它们是死的,它们将被回收。

Lots of things are known roots. Static fields, for example, are all known roots. Local variables might be known roots, but as we'll see below, they can be optimized away in surprising ways. Temporary values might be known roots.

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

I'm creating an instance of a class without assigning it to a variable.

我正在创建一个类的实例,而不将其赋值给一个变量。

Your question here is a good one but it is based on an incorrect assumption, namely that a local variable is always a known root. Assigning a reference to a local variable does not necessarily keep an object alive. The garbage collector is allowed to optimize away local variables at its whim.

你的问题很好,但它基于一个错误的假设,即局部变量总是已知的根。给局部变量赋值不一定能使对象保持活动状态。垃圾收集器可以随意地优化本地变量。

Let's give an example:

让我们举个例子:

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

Suppose resource is an instance of a class that has a finalizer, and the finalizer closes the file. Can the finalizer run before MessWithFile? Yes! The fact that resource is a local variable with a lifetime of the entire body of M is irrelevant. The runtime can realize that this code could be optimized into:

假设资源是具有终结器的类的实例,终结器关闭文件。终结器可以在MessWithFile之前运行吗?是的!资源是一个局部变量,整个M的生命周期是不相关的。运行时可以意识到该代码可以优化为:

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

and now resource is dead by the time MessWithFile is called. It is unlikely but legal for the finalizer to run between GetHandle and MessWithFile, and now we're messing with a file that has been closed.

现在,在调用MessWithFile时,资源已经死亡。终结器在GetHandle和MessWithFile之间运行是不可能的,但是它是合法的,现在我们正在处理一个已经关闭的文件。

The correct solution here is to use GC.KeepAlive on the resource after the call to MessWithFile.

这里的正确解决方案是使用GC。在调用MessWithFile之后保持资源的活动。

To return to your question, your concern is basically "is the temporary location of a reference a known root?" and the answer is usually yes, with the caveat that again, if the runtime can determine that a reference is never dereferenced then it is allowed to tell the GC that the referenced object might be dead.

回到你的问题,你的问题基本上是“临时位置的一个引用一个已知根?”的答案通常是肯定的,但需要说明的是,再一次,如果运行时可以确定一个参考从未引用时则允许告诉GC引用的对象可能会死。

Put another way: you asked if

换句话说:你问我。

new MyClass().DoSomething();

and

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

are the same from the point of view of the GC. Yes. In both cases the GC is allowed to kill the object the moment that it determines it can do so safely, regardless of the lifetime of local variable c.

从GC的角度来看是一样的。是的。在这两种情况下,GC都被允许在它确定它可以安全的情况下杀死这个对象,而不考虑本地变量c的生命周期。

The shorter answer to your question is: trust the garbage collector. It has been carefully written to do the right thing. The only times you need to worry about the GC doing the wrong thing are scenarios like the one I laid out, where timing of finalizers is important for the correctness of unmanaged code calls.

简短的回答是:信任垃圾收集器。为了做正确的事,它被小心地写了出来。您只需要担心GC做了错误的事情,就像我所描述的场景一样,终结器的时间对非托管代码调用的正确性非常重要。

#3


6  

Of course, GC is transparent to you and no early collection can ever happen. So I guess you want to know the implementation details:

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

An instance method is implemented like a static method with an additional this parameter. In your case the this value lives in registers and is passed like that into DoSomething. The GC is aware what registers contain live references and will treat them as roots.

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

As long as DoSomething might still use the this value it stays live. If DoSomething never uses instance state then indeed the instance can be collected while a method call is still running on it. This is unobservable, therefore safe.

只要DoSomething仍然使用这个值,它就会一直存活。如果DoSomething从未使用实例状态,那么确实可以在方法调用仍在其上运行时收集实例。这是不可观测的,因此是安全的。

#4


4  

As long as you're talking about a single threaded environment, you're safe. Fun things only start to happen if you're starting a new thread inside the DoSomething method, and even more fun happens if your class has a finalizer. The key thing to understand here is that a lot of the contracts between you and the runtime / optimizer / etc. are valid only in a single thread. This is one of the things that has disastrous results when you start programming on multiple threads in a language that isn't primaririly multi-threading oriented (yes, C# is one of those languages).

只要您谈论的是单线程环境,您就安全了。只有当您在DoSomething方法中启动一个新线程时,才会发生有趣的事情;如果您的类有终结器,则会发生更有趣的事情。这里要理解的关键是,您与运行时/优化器等之间的许多契约只在一个线程中有效。当您开始在一种非原始的面向多线程的语言(是的,c#是这些语言之一)上对多个线程进行编程时,这是一件很糟糕的事情。

In your case, you're even using the this instance, which makes unexpected collection even less likely while still inside that method; in any case, the contract is that on a single thread, you can't observe the difference between the optimized and unoptimized code (apart from memory usage, speed, etc., but those are the "free lunch").

在你的例子中,你甚至在使用这个实例,这使得在这个方法中,意外收集的可能性更小;无论如何,契约是在一个线程上,您无法观察优化代码和未优化代码之间的区别(除了内存使用、速度等,但这些都是“免费午餐”)。