Objects in arrays do not collect garbage

I tested a class that uses weak references to ensure that objects can be garbage collected, and I found that objects in List <> were never collected, even if the list was no longer referenced. This also applies to 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 the initialization of the array, the test passes.

Why doesn't the object in the array get garbage collection?

+6
source share
5 answers

Another edit: the result will always be false if the array is defined in Main () - Method-Scope, but will be true if it is defined in the test class. Maybe this is not bad.

 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); } } 
+2
source

EDIT: Okay, I have made some progress on this. There are three binary switches involved (at least):

  • Is the code optimized? that is, the /o+ or /o- flag on the command line. It does not seem to make any difference.
  • Whether the code runs in the debugger or not. It does not seem to make any difference.
  • The level of generated debug information, i.e. command line flag /debug+ , /debug- or /debug:full or /debug:pdbonly . Only /debug+ or /debug:full fail.

Additionally:

  • If you separate the Main code from the TestDestructor code, you can say that this is the Main code compilation mode, which makes the difference
  • As far as I can tell, the IL generated for /debug:pdbonly is the same as for /debug:full inside the method itself, so there might be a clear problem ...

EDIT: Alright, now it's really weird. If I make out the "broken" version and then put it together again, it works:

 ildasm /out:broken.il Program.exe ilasm broken.il 

ilasm has three different debugging settings: /DEBUG , /DEBUG=OPT and /DEBUG=IMPL . Using one of the first two, it fails - using the last, it works. The latter is described as enabling JIT optimization, so apparently it matters here ... although, in my opinion, it should still collect the object anyway.


Perhaps this is due to the memory model in terms of DestructorCalled . This is not mutable, therefore there is no guarantee that the record from the finalizer stream will be "visible" by your test stream.

Finalizers are certainly called in this scenario. After creating the volatile variable, this separate, equivalent example (which is just easier to run for me) certainly prints True for me. This is not proof, of course: without volatile code will not crash; it just isn't guaranteed to work. Can you run the test after a failure by making it a mutable 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 just saw how it happened when building with Visual Studio, but it was fine from the command line. A look at the IL now ...

+4
source

As Ani pointed out in the comments, the whole array is optimized in release mode, so we need to 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 (no volatility) and always prints True. Can someone confirm that the finalizer is not called in release mode, because otherwise we can assume that it is associated with debug mode.

+1
source

If I am not mistaken, because the object is essentially copied and separated from it by the initial creation when loading into an array. Then, when you destroy the array and the original object, the object copied to the array still exists.

Garbage collection should ultimately do the job, but I understand that you are trying to get it to clean up resources. I would try to clear the array first (by deleting the object) before destroying it, and see if it gets rid of everything.

0
source

Something that the documentation says:

Implementing Finalize methods or destructors can adversely affect performance, and you should avoid using them unnecessarily. To recover the memory used by objects using Finalize methods, at least two garbage collections are required. [...] Future garbage collection will determine that finalized objects are truly garbage because they no longer point to entries in the list of objects marked as ready for finalization. In this future garbage collection, object memory is actually recovered.

Try using a recycling mechanism instead of completing to see what happens.

0
source

Source: https://habr.com/ru/post/901279/


All Articles