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).
I doubt that it's really anything to do with
call
andcallvirt
.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.
When you're debugging, the whole system1 extends the lifetime of objects beyond their natural lifetime.
So, when a thread is executing this method:
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 inspectthis
. So the system conspires to keepthis
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.