1

My .NET Core 2.1 application, running on Windows x64, allocates large arrays of structs, each struct holding a class in a field. The arrays end up on the Large Object Heap, so they are not collected until a full (gen 2) GC. That was expected. What surprised me a bit is that the classes referenced by the structs also don't seem to get GCed until a gen 2 GC. Is this expected behaviour?

Something like this happens if I have a large HashSet as well, because internally a HashSet keeps an array of HashSet.Slot - a struct.

Some code that reproduces the problem:

    class MyClass
    {
        public long Value;

        ~MyClass()
        {
            if (Value < 10)
            {
                Console.WriteLine("MyClass finalizer " + Value);
            }
        }
    }

    struct TheEntry
    {
        public MyClass TheClass;
    }

    class Program
    {
        static void Main(string[] args)
        {
            var wr = AllocateSomeStuff();
            Console.WriteLine("Before GC MyClass is in gen " + GC.GetGeneration(wr));

            GC.Collect(1);
            Console.WriteLine("GC done");
            GC.WaitForPendingFinalizers();
            Console.WriteLine("Finalizers done");
            Console.WriteLine("After GC MyClass is in gen " + GC.GetGeneration(wr));
        }

        private static WeakReference<MyClass> AllocateSomeStuff()
        {
            var array = new TheEntry[11000];

            for (int i = 0; i < array.Length; ++i)
            {
                array[i].TheClass = new MyClass { Value = i };
            }

            return new WeakReference<MyClass>(array[0].TheClass);
        }
    }

When I run this with an array of 11,000 elements on a 64-bit system (so it's over the 85KB threshold for LOH) the finalizers for MyClass do not run. Output:

Before GC MyClass is in gen 0
GC done
Finalizers done
After GC MyClass is in gen 1

With 10,000 elements they run. I suppose I expected the runtime to GC MyClass as it's unreachable, even though it's still referenced by another unreachable object. Is this how it's supposed to work?

EM0
  • 5,369
  • 7
  • 51
  • 85
  • When I compile your exact code as a "Release" configuration build and run without the debugger, I see the finalizers run, just as you want. I.e. there are ten lines of `MyClass finalizer #` (where `#` is a value from 0 to 9) between `GC done` and `Finalizers done`. In any case, it's the GC's prerogative as to whether to collect an object or not. It's my view that you haven't provided a [mcve] that demonstrates the behavior you claim to be observing, but even if you did, non-deterministic memory management necessarily means you might at times see behavior different from your own expectations. – Peter Duniho Sep 24 '19 at 18:16
  • @PeterDuniho I've got the same using Debug configuration with and without debugger – Pavel Anikhouski Sep 24 '19 at 18:31
  • Don't use the "Debug" configuration. That disables a variety of optimizations, including some related to GC. You won't get reliable information that way. Only a "Release" build without a debugger attached will give you real-world behavior (but still keep in mind that, even then, **garbage collection is non-deterministic**...in a real-world program, there are lots of things that can change the way the GC will work) – Peter Duniho Sep 24 '19 at 18:38
  • I get the same behaviour in Debug and Release builds, consistently. So this is a minimal reproducible example for me. If you say it doesn't behave the same way on your system - I believe you, but I don't know why. – EM0 Oct 01 '19 at 12:38

0 Answers0