Garbage Collector, call & callvirt and Debug/Relea

2019-07-12 03:51发布

I have a class:

public class SomeClass {
    public int I;

    public SomeClass(int input) {
        I = input;
        Console.WriteLine("I = {0}", I);
    }

    ~SomeClass() {
        Console.WriteLine("deleted");
    }

    public void Foo() {
        Thread.Sleep(1000);
        Console.WriteLine("Foo");
    }
}

and this program:

class Program {
    static void Main(string[] args) {
        new Thread(() => {
                       Thread.Sleep(100);
                       GC.Collect();
                   }) { IsBackground = true }.Start();

        new SomeClass(10).Foo();

        // The same as upper code
        // var t = new SomeClass(10);
        // t.Foo();
    }
}

When i run this code in Debug mode, i have next result:

I = 10
Foo
deleted

but, when i change mode to Release, result changes to:

I = 10
deleted
Foo

As i understand, there is difference with call and callvirt: when optimization starts in Release mode, compiler look at Foo method and can't find any reference to SomeClass in this method, and that's why this method calls as static method by address, and garbage collector could collect this object.

Otherwise, if i change Foo method (for example, add Console.WriteLine(I) into this method), compiler won't decide to call this method as call and it should be called by pointer to instance with callvirt and garbage collector won't collect this object.

Can you please explain more clearly, what's going on here (why GC could collect object and if it's so, how does method calls).

2条回答
ゆ 、 Hurt°
2楼-- · 2019-07-12 04:05

I doubt that it's really anything to do with call and callvirt.

I strongly suspect it's simply because you're not using any fields within SomeClass.Foo. The garbage collector is free to collect an object when it is certain that none of the data will be referenced again - so no code is going to look at any references to the object, or any fields within the object.

Basically, if you write a finalizer and you need to ensure that the object isn't finalized while methods within that object are running, you need to be very careful. You can use GC.KeepAlive(this) at the end of methods, as one approach, but it's a bit ugly. I would personally try very hard to avoid needing a finalizer at all, if you can. I can't remember the last time I wrote one. (See Joe Duffy's blog for more.)

When there's a debugger attached, the GC is much less aggressive about what it can collect - after all, if you can break into the debugger at any point and inspect the fields of any object that is the target of a running instance method, that removes the possibility of garbage collecting those objects.

查看更多
欢心
3楼-- · 2019-07-12 04:14

When you're debugging, the whole system1 extends the lifetime of objects beyond their natural lifetime.

So, when a thread is executing this method:

public void Foo() {
    Thread.Sleep(1000);
    Console.WriteLine("Foo");
}

This method makes no use of this. So when not debugging, after this method has started running, it doesn't require the object to exist any longer.

However, when debugging, you might have set a breakpoint on that Console.WrtieLine method, and from there decide to inspect this. So the system conspires to keep this alive (as well as any local variables which are no longer used within the body of a method).


1This old presentation shows how the JIT and GC actually work together to work out which references are live (see slide 30 onwards). It is my understanding that the JIT does more work to help out with debugging - by not reusing local variable slots which it could (so values are still visible) and by informing the GC that all variables are alive throughout the method, rather than the more piecemeal analysis it can provide.

查看更多
登录 后发表回答