2

I have a test program that implements ManagedClass with both Dispose method and a Finalizer.
I'm forcing garbage collection via GC.Collect(), but Finalizer is only called if an object is created in a separate method. Why is that?

namespace FinalizerTest
{
    internal class Program
    {
        
        static void Run()
        {
            ManagedClass mc = new ManagedClass();
            mc.StartWriting();
        }
        
        static void Main(string[] args)
        {
            Console.WriteLine("Let's test Finalizers!");
            
            // Test #1: Create object through another method
            Console.WriteLine("Test #1");
            Run();
            GC.Collect();
            GC.WaitForPendingFinalizers();

            // Test #2: Create object directly in Main
            Console.WriteLine("Test #2");
            ManagedClass mc1 = new ManagedClass();
            mc1.StartWriting();
            mc1 = null;

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

            Console.ReadLine();
        }
    }
    public class ManagedClass : IDisposable
    {
        private StreamWriter _writer;
        
        public void StartWriting()
        {
            _writer = new StreamWriter("output.txt");
        }
        
        public void Dispose()
        {
            Console.WriteLine("Disposing");

            _writer?.Dispose();

            GC.SuppressFinalize(this);
        }

        ~ManagedClass()
        {
            Console.WriteLine("Finalizing");

            _writer?.Dispose();
        }
    }
}

As I understand it in both scenarios a reference to the object is lost.

  • First test - reference is lost by exiting the scope of the method

  • Second test - reference is lost by setting reference variable to null

However, Finalizer is consistently only ever called after the first test.

According to Microsoft documentation:

The garbage collector then calls the Finalize method automatically under the following conditions:

  • After the garbage collector has discovered that an object is inaccessible, unless the object has been exempted from finalization by a call to the GC.SuppressFinalize method.

I'm not calling GCSuppressFinalize and according to Visual Studio I'm working on .NET 7.0

I tried:

  1. Reassigning a new reference instead of null

  2. Implementing Finalizer without the Dispose method

  3. Running in Release configuration / building the project and running the application directly through exe file

None of those things called Test #2 Finalizer

Noby
  • 57
  • 6
  • 1
    Most likely you run it in debug mode. Compile in release and run without debugger. In this case you also don't need to assign it to null - runtime is smart enough to detect it's not used anywhere after mc1.StartWriting – Evk Jan 22 '23 at 19:58
  • 1
    GC.Collect() is not deterministic. When you tell it to run and kick off a garbage collection cycle, in this case as a first step the GC will likely see there is no memory pressure at all, and then immediately quit. Just because you run it doesn't mean anything of consequence happens. Also, it is **VERY RARE** to need to write a finalizer at all, even when dealing with unmanaged resources. And if in fact you **should not** implement either IDisposable or a finalizer for types that only have managed resources. – Joel Coehoorn Jan 22 '23 at 20:36
  • @JoelCoehoorn I disagree with you. MS [states](https://learn.microsoft.com/en-us/dotnet/api/system.gc.collect?view=net-7.0#system-gc-collect) that `GC.Collect()` will **force** an immediate garbage collection of all generations. – Artur Jan 22 '23 at 21:20
  • @JoelCoehoorn I know that Finalizers ideally shouldn't ever be called. I'm just studying the whole GC cycle and how it all works. The problem I'm having is that it **consistently** doesn't call upon the Finalizer in 2nd test for some reason and all Documentation says is that the difference between .NET Core and Framework is that Framework can also call upon Finalizers during shutdown of an application domain. Except when I tried to move my application to .NET Framework it started working despite Test 2 finalizer being called before shutdown – Noby Jan 22 '23 at 22:15
  • Speaking of which, should I update the question regarding the fact that on Framework it does indeed work as expected? – Noby Jan 22 '23 at 22:16
  • Not just shouldn't be called: shouldn't be written at all. – Joel Coehoorn Jan 22 '23 at 22:38
  • Does this answer your question? [Collect objects still in scope - GC.Collect](https://stackoverflow.com/questions/6425052/collect-objects-still-in-scope-gc-collect) – Charlieface Jan 23 '23 at 14:54
  • In Debug mode the life of an object is extended to the end of the function. Setting to null does not help. In Release mode even objects that are in scope can still be collected if they are not used anymore. – Charlieface Jan 23 '23 at 14:55
  • @Charlieface I don't think so. I ran the test in Release config and even built the project and run it directly through exe file just in case. Neither of them called Test #2 Finalizer – Noby Jan 27 '23 at 10:03
  • You know that the finalizer isn't guaranteed to run? – Enigmativity Jul 03 '23 at 00:15

1 Answers1

0

Sometimes GC.Collect doesn't work perfectly.

To improve the garbage collection, I call the following procedure :

internal static void CollectGarbage(int SizeToAllocateInMo)
    { // set SizeToAllocateInMo to -1 to force garbage collection without testing available memory. 
      long [,] TheArray ;
      bool DoCollect=SizeToAllocateInMo<0 ;
      if (!DoCollect) try { TheArray = new long[SizeToAllocateInMo,125000] ; } catch { DoCollect=true ; }
      TheArray=null ;
      if (DoCollect) 
      { 
        GC.Collect() ; 
        GC.WaitForPendingFinalizers() ; 
        SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle,-1,-1) ;
        GC.Collect() ; 
      }
    }

[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]

private static extern int SetProcessWorkingSetSize(IntPtr process, int minimumWorkingSetSize, int maximumWorkingSetSize); // used for garbage collection
Graffito
  • 1,658
  • 1
  • 11
  • 10