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?
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.
class TestDestructor
{
public TestDestructor()
{
testList = new List<string>();
}
public static volatile bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
public string xy = "test";
public List<string> testList;
}
class Test
{
private static object[] myArray;
static void Main()
{
NewMethod();
myArray = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(TestDestructor.DestructorCalled);
Console.In.ReadToEnd();
}
private static void NewMethod()
{
TestDestructor testDestructor = new TestDestructor() { xy = "foo" };
testDestructor.testList.Add("bar");
myArray = new object[] { testDestructor };
Console.WriteLine(myArray.Length);
}
}
EDIT: Okay, I'm making some progress on this. There are three binary switches potentialy involved (at least):
- Whether the code is optimized; i.e. the
/o+
or /o-
flag on the command line. This seems to make no difference.
- Whether the code is run in the debugger or not. This seems to make no difference.
- The level of debug information generated, i.e. the
/debug+
, /debug-
or /debug:full
or /debug:pdbonly
command line flag. Only /debug+
or /debug:full
causes it to fail.
Additionally:
- If you separate the
Main
code from the TestDestructor
code, you can tell that it's the compilation mode of the Main
code which makes the difference
- As far as I can tell, the IL generated for
/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:
ildasm /out:broken.il Program.exe
ilasm broken.il
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?
using System;
class TestDestructor
{
public static volatile bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
}
class Test
{
static void Main()
{
TestDestructor testDestructor = new TestDestructor();
var array = new object[] { testDestructor };
array = null;
testDestructor = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(TestDestructor.DestructorCalled);
}
}
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...
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:
class TestDestructor
{
public static bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
}
class Test
{
static void Main()
{
TestDestructor testDestructor = new TestDestructor();
var array = new object[] { testDestructor };
Console.WriteLine(array[0].ToString());
array = null;
testDestructor = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(TestDestructor.DestructorCalled);
}
}
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.
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:
Implementing Finalize methods or destructors can have a negative impact on performance and you should avoid using them unnecessarily. Reclaiming the memory used by objects with Finalize methods requires at least two garbage collections. [...] A future garbage collection will determine that the finalized objects are truly garbage because they are no longer pointed to by entries in the list of objects marked as ready for finalization. In this future garbage collection, the objects' memory is actually reclaimed.
Try to use a mechanism of disposing instead of finalizing to see what will happen