Nesting reference type object in inner scope has n

2019-06-26 02:32发布

问题:

I have been reading the Garbage Collection chapter from Jeffrey Richter's fine book, "CLR via C#". There, he illustrated an example of how the GC conceptually works (how roots are marked) by referring to a disassembly listing of native code emitted from the JIT compiler. From that example, it occurred to me that nesting reference types in scope seems to have ZERO effect on expediting the garbage collection of the nested variable. I wonder if I am understanding this correctly. In any case, consider these 2 versions of code:

A) Nesting a reference type variable (y) in an inner scope:

namespace scope
{
    class A { public void foo() { } }
    class Program
    {
        static void Main(string[] args)
        {
            A x = new A();
            x.foo();
            {
                A y = new A();
                y.foo();
            }
        }
    }
}

B) Same as above, except that x and y are in the same scope.

namespace scope
{
    class A { public void foo() { } }
    class Program
    {
        static void Main(string[] args)
        {
            A x = new A();
            x.foo();

            A y = new A();
            y.foo();
        }
    }
}

Out of curiosity, I checked the generated IL code for both versions, and they are the SAME!

Q1: So, this seems to imply that indeed, scoping doesn't expedite garbage collection in any way. Is this correct? (Note: I know about the "using" statement - but I'm only curious about the behaviour on the GC of "plain old scoping" as illustrated in the 2 examples above.)

Q2: If the answer to Q1 is "True", then I am utterly perplexed by how a "Object Lifetime is Not Determined by Scope" situation can possibly happen, as described here: http://www.curly-brace.com/favorite.html

回答1:

Garbage collection partly depends on whether you're running in the debugger or not. I'll assume we're not.

It can actually be much more aggressive than you think. For example:

object o = new object();
Console.WriteLine(o);   
Console.WriteLine("hi");
o = new object();

The object can be garbage collected immediately after the second line - i.e. long before the variable o exits scope. This would also be true without the final line.

In other words, scoping doesn't expedite garbage collection - because the GC is already smarter than you might think.

In fact, the garbage collector can be even more aggressive. Consider this code:

Foo f = new Foo();
f.SomeMethod();
Console.WriteLine("Hello");

where Foo looks like this:

public class Foo
{
    int x = 10;

    public void SomeMethod()
    {
        Console.WriteLine(x);
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Hello");
            Thread.Sleep(100);
        }
    }
}

In theory, the garbage collector can collect the Foo object while SomeMethod is running so long as it's gone past the first line, where it actually reads from x. If Foo had a finalizer, you could have the finalizer running in one thread while SomeMethod was running in another. Scary stuff.



回答2:

Jon is, of course correct. For some additional background I refer you to the C# specification, which states:

The actual lifetime of a local variable is implementation-dependent. For example, a compiler might statically determine that a local variable in a block is only used for a small portion of that block. Using this analysis, the compiler could generate code that results in the variable’s storage having a shorter lifetime than its containing block.

and, also important:

The storage referred to by a local reference variable is reclaimed independently of the lifetime of that local reference variable.

Notice here that we are drawing a distinction between the lifetime of the object being referenced -- that is, when the garbage collector is allowed to step in -- and the lifetime of the variable holding the reference. As Jon points out, the lifetime of both the variable and the object it refers to can be aggressively shortened by the compiler and the garbage collector both if we can prove that doing so does not release a reference that someone could still be holding on to.



回答3:

The author of the linked page appears to think that C# is (or should be) in some way related to C++. From such a position it is not surprising that 'astonishing' (in the Principle of Least Astonishment sense) things follow. In particular,

A ReadAccessor is an object which provides access to the pixel data in an image, which is stored in a memory mapped file for performance reasons.

should suggest immediately (at a really quite basic level of knowledge of .NET) that ReadAccessor implement IDisposable.



回答4:

Object life-time doesn't need scope, it is based on references to that object and it's current generation count in the GC - once it is completely dereferenced, it's time-to-GC just depends on its generation.

Method scoped references will simply be completely dereferenced by the end of the method (in your example).

Update: or as Jon points out, if it's completely derefenced before then, it is elligible for GC.