2
votes

According to Fundamentals of garbage collection, all threads except the one that triggered the garbage collection are suspended during garbage collection. Since the finalizers are called during the garbage collection process, I would expect the thread to be suspended until all the finalizers are executed. Thus, I would expect the following code to just "take longer" to complete. Instead, it throws a System.OutOfMemoryException. Could someone elaborate on why this happens?

class Program
{
    class Person
    {
        long[] personArray = new long[1000000];
        ~Person()
        {
            Thread.Sleep(1);
        }
    }

    static void Main(string[] args)
    {
        for (long i = 0; i < 100000000000; i++)
        {
            Person p = new Person();
        }
    }
}

Wouldn't the creation of new Person objects on the heap also be suspended while the finalizer is being executed?

Code taken from Exam Ref 70-483 Programming in C# (MCSD)

1
Which .NET version are you using so far? Also, there are two quite helpful articles about finalizers, When everything you know is wrong, part one and When everything you know is wrong, part two – Pavel Anikhouski
I am not a C# developer (literally have not written a single line in that), but most-probably some phases of the GC are concurrent, like scanning. while GC does that phase, your allocation rate is just too high for it to finish... – Eugene
@Eugene it’s yet another example why giving finalizers a destructor syntax was a bad idea. – Holger

1 Answers

0
votes

Okay, so I did some more research, and it turns out that the garbage collector does not call the finalizers synchronously. It does the following:

  1. Freezes all running threads.
  2. Adds items that need finalization to the finalizer queue.
  3. Performs garbage collection on eligible objects (objects with no finalizers or with finalizers already called.)
  4. Thaws the threads that it froze.

After that, the finalizer thread starts running in the background along with the rest of the application, and calls the finalizers of each object in its queue. This is where the issue I described arises. The Person objects in the heap have their references moved into the finalization queue, but their memory is not freed up until the finalization actually happens. The finalization takes place while the application is running (and more objects are created on the heap).

The garbage collector cannot reclaim the memory until finalization is over, thus, a race condition arises between the Main Thread (creating the objects) and the GC Finalizer Thread (calling the finalizers).

I have also confirmed this behaviour by debugging the IL code and seeing the execution switch between the two threads described above.

Main Thread and GC Finalizer Thread running in parallel

I have also found this question on SO which describes the same behaviour.