2

Steps to Reproduce:

This is my code:

    using (TestClass test = new TestClass())
    {

    }

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

This is how I defined TestClass:

public class TestClass : IDisposable
{
    public void Dispose()
    {
        Dispose(true);
        //GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            Console.WriteLine("Disposing TestClass.");
        }
    }

    ~TestClass()
    {
        Dispose(false);
        Console.WriteLine("TestClass Finalizer called");
    }
}

Expected Behavior: I do see "Disposing TestClass" being printed after the using statement as expected but I also expect "TestClass Finalizer called" to be printed after the GC commands I run. I ensured that I skip calling GC.SuppressFinalize(this); in the Dispose method. Looks like disposed variables don't get finalized even after they are out-of-scope. They seem to get finalized just before the program exits.

Actual Behavior: I only see "Disposing TestClass" being printed after the using statement as expected only don't see "TestClass Finalizer called" after the GC commands. I only see it just before the program exits.

Isn't this considered a memory leak?

If I convert this to a non-disposable class and update the code like below, I do see the finalizer being called after the GC commands.

TestClass test = new TestClass();
test = null;

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();


public class TestClass
{
    ~TestClass()
    {   
        Console.WriteLine("TestClass Finalizer called");
    }
}
Rajesh Nagpal
  • 1,088
  • 1
  • 9
  • 12
  • 1
    Golden Rule: IDisposable does not affect the way the garbage collector behaves. That your implementation doesn't do anything useful does not affect it either. Select the Release configuration and press Ctrl+F5, now it works. [Explained here](http://stackoverflow.com/a/17131389/17034). – Hans Passant Feb 19 '17 at 16:32
  • The finaliser won't run until the object is collected. There's no reason to suspect it will be collected in the first example (it could, and conversely there's no guarantee it would be in the second, but the outcomes are certainly the most likely behaviour). – Jon Hanna Feb 19 '17 at 18:04
  • Thanks Hans for the reference! – Rajesh Nagpal Feb 27 '17 at 17:21

4 Answers4

4

According to Microsoft's description of the Object.Finalize Method ()

The exact time when the finalizer executes is undefined. To ensure deterministic release of resources for instances of your class, implement a Close method or provide a IDisposable.Dispose implementation.

Look at finalizers as a second line of defense. If the program fails to call Close or Dispose, then the finalizer will get a change to correct the omission.

This would still ensure, that a file would be closed at program exit, for instance, unless another finalizer is not exiting and is therefore blocking other finalizers or if a catastrophic exception is brutally terminating the program.

This is not a memory leak. If the memory becomes scarce, the Garbage Collector (GC) can decide to free resources and call finalizers long before the program exits.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
2

Calling the Dispose method does not affect the runtime of an object. The GC only collect object which are no longer referenced (exceptions apply). In your first example the test variable is never garbage collected (by your GC.Collect statement) as it is declared inside of the containing method and not in the scope of the using block.

Given the following C# input (A implementing IDisposable):

using (var a = new A())
{

}

The following IL code is emitted:

IL_0001: newobj       instance void Testit.A::.ctor()
IL_0006: stloc.0      // a
.try
{

  // [10 13 - 10 14]
  IL_0007: nop          

  // [12 13 - 12 14]
  IL_0008: nop          
  IL_0009: leave.s      IL_0016
} // end of .try
finally
{

  IL_000b: ldloc.0      // a
  IL_000c: brfalse.s    IL_0015
  IL_000e: ldloc.0      // a
  IL_000f: callvirt     instance void [mscorlib]System.IDisposable::Dispose()
  IL_0014: nop          
  IL_0015: endfinally   
} // end of finally

If we were to convert it into C# this would be the ~ result:

A a;
try
{
}
catch
{
    if (a != null)
    {
        a.Dispose();
    }
}

The variable a has a method scope and therefore is only collected if the method is left. That is why the finalizer is not called.

In your second example you are deliberately setting your instance to null removing the instance from scope and as there is no active pointer the instance gets collected.

a-ctor
  • 3,568
  • 27
  • 41
  • It is not a matter of scope. It is a matter of reachability. If you have a linked list, for instance, then you might have a variable in scope that points to the head of the list; however, objects in the middle of the list will not be in scope of any running code, but still be reachable. – Olivier Jacot-Descombes Feb 19 '17 at 17:02
  • @OlivierJacot-Descombes I was referring to his example in which the `test` variable is not going out of scope after the `using` directive ends as one might think. Bad phrasing on my side I guess. – a-ctor Feb 19 '17 at 17:42
  • Scope is about where you can use a variable in a program. Reachability depends on where you did use it, and after optimisations were applied, so something can be collected while in scope and uncollectable while out of scope. – Jon Hanna Feb 19 '17 at 18:02
  • You were right in how the using statement gets converted by the compiler and hence why it doesn't gets GC'ed. As I mentioned in my answer, if you run the same thing in Release mode, it gets GC'ed and finalizer getting called. – Rajesh Nagpal Feb 27 '17 at 17:25
0

I guess that despite you set the test variable to null there is still a reference to it, it happens on some .NET versions in the debug mode. Try this code

private void CreateAndRelease()
{
  new TestClass();
}

public void Main()
{
  CreateAndRelease();
  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.ReadLine();
}

Note, that GC.WaitForPendingFinalizers is called twice - first GC.Collect will not finalize your object but only place it into finalization queue, only on next garbage collection it will be finalized.

EDIT: Some shy person downvoted my answer, but I checked it before posting unlike they. .NET 4.5.1 Debug mode

Code and output 1:

Output:
Disposing TestClass.
Finished

public static void Main()
{
  using (TestClass test = new TestClass())
  {}

  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.WriteLine("Finished");
  Console.ReadLine();
}

Code and output 2:

Disposing TestClass.
TestClass Finalizer called
Finished

public static void Main()
{
  CreateAndRelease();

  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
  GC.WaitForPendingFinalizers();

  Console.WriteLine("Finished");
  Console.ReadLine();
}

private static void CreateAndRelease()
{
  using (TestClass test = new TestClass())
  {}
}

So my answer is absolutely correct and will help topic starter.

Ed Pavlov
  • 2,353
  • 2
  • 19
  • 25
  • I believe you don't need the second GC.WaitForPendingFinalizers(); The second GC.Collect() will collect the reference after it has been finalized by the GC.WaitForPendingFinalizers(); call first time. – Rajesh Nagpal Feb 27 '17 at 17:23
0

Here is the reason for this behavior:

The following code:

using (TestClass test = new TestClass())
{

}

Gets compiled to this:

TestClass test;
try
{
test = new TestClass();
}
finally
{
if(test != null)
      test.Dispose();
}

For the same reasons as explained in Strange behaviour while Unit testing for memory leak using WeakReference, when you run this in Debug mode, the "test" variable is alive until the end of the method and hence it's not GC'ed and finalizer not getting called. If you run the same thing in Release mode, it will work as expected.

For the non-disposable type(in my second part), since there is a way in which you can assign this "test" variable to null and hence GC can collect it even when used in Debug mode and hence it works as expected.

Community
  • 1
  • 1
Rajesh Nagpal
  • 1,088
  • 1
  • 9
  • 12