Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objects in arrays are not getting garbage collected

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?

like image 705
Paul Haley Avatar asked Nov 11 '11 08:11

Paul Haley


3 Answers

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...

like image 92
Jon Skeet Avatar answered Oct 19 '22 22:10

Jon Skeet


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);
    }
}
like image 2
Jobo Avatar answered Oct 19 '22 21:10

Jobo


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.

like image 1
Stilgar Avatar answered Oct 19 '22 23:10

Stilgar