I was testing a class that uses weak references to ensure that objects were able to be garbage collected and I found that objects in a List<> were never collected even if the list is no longer referenced. This is also the case with a simple array. The following code snippet shows a simple test that fails.
class TestDestructor
{
public static bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
}
[Test]
public void TestGarbageCollection()
{
TestDestructor testDestructor = new TestDestructor();
var array = new object[] { testDestructor };
array = null;
testDestructor = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(TestDestructor.DestructorCalled);
}
Leaving out the initialisation of the array causes the test to pass.
Why is the object in the array not getting garbage collected?
EDIT: Okay, I'm making some progress on this. There are three binary switches potentialy involved (at least):
/o+
or/o-
flag on the command line. This seems to make no difference./debug+
,/debug-
or/debug:full
or/debug:pdbonly
command line flag. Only/debug+
or/debug:full
causes it to fail.Additionally:
Main
code from theTestDestructor
code, you can tell that it's the compilation mode of theMain
code which makes the difference/debug:pdbonly
is the same as for/debug:full
within the method itself so it may be a manifest issue...EDIT: Okay, this is now really weird. If I disassemble the "broken" version and then reassemble it, it works:
ilasm has three different debug settings:
/DEBUG
,/DEBUG=OPT
, and/DEBUG=IMPL
. Using either of the first two, it fails - using the last, it works. The last is described as enabling JIT optimization, so presumably that's what's making a difference here... although to my mind it should still be able to collect the object either way.It's possible that this is due to the memory model in terms of
DestructorCalled
. It's not volatile, so there's no guarantee that the write from the finalizer thread is "seen" by your test thread.Finalizers certainly are called in this scenario. After making the variable volatile, this standalone equivalent example (which is just simpler for me to run) certainly prints True for me. That's not proof, of course: without
volatile
the code isn't guaranteed to fail; it's just not guaranteed to work. Can you get your test to fail after making it a volatile variable?EDIT: I've just seen this fail when built with Visual Studio, but it was fine from the command line. Looking into the IL now...
If I'm not mistaken it's because the object is essentially copied and separate from it's initial creation when loaded into the array. Then when you destroy the array and original object, the object copied to the array still exists.
Garbage Collection should do it's job eventually, but I get that you're trying to force it to clear resources. What I would try is to clear the array first (removing the object) before destroying it, and see if it gets rid of everything.
That's what documentation says:
Try to use a mechanism of disposing instead of finalizing to see what will happen
As pointed out by Ani in the comments the whole Array is optimized away in release mode so we should change the code to look like this:
For me it works (without volatile) and prints True always. Can anyone confirm that the finalizer is not called in release mode because otherwise we can assume it is related to debug mode.
Another edit: result will be always false if the array is defined in the Main()-Method-Scope, but will be true if defined in the Class-Test-Scope. Maybe thats not a bad thing.