Why does the c# garbage collector not keep trying to free memory until a request can be satisfied?
Asked Answered
V

2

9

Consider the code below:

using System;

namespace memoryEater
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("alloc 1");
            var big1 = new BigObject();

            Console.WriteLine("alloc 2");
            var big2 = new BigObject();

            Console.WriteLine("null 1");
            big1 = null;

            //GC.Collect();

            Console.WriteLine("alloc3");
            big1 = new BigObject();

            Console.WriteLine("done");
            Console.Read();
        }
    }

    public class BigObject
    {
        private const uint OneMeg = 1024 * 1024;
        private static int _idCnt;
        private readonly int _myId;
        private byte[][] _bigArray;

        public BigObject()
        {
            _myId = _idCnt++;
            Console.WriteLine("BigObject {0} creating... ", _myId);

            _bigArray = new byte[700][];

            for (int i = 0; i < 700; i++)
            {
                _bigArray[i] = new byte[OneMeg];
            }

            for (int j = 0; j < 700; j++)
            {
                for (int i = 0; i < OneMeg; i++)
                {
                    _bigArray[j][i] = (byte)i;
                }
            }
            Console.WriteLine("done");
        }

        ~BigObject()
        {
            Console.WriteLine("BigObject {0} finalised", _myId);
        }
    }
}

I have a class, BigObject, which creates a 700MiB array in its constructor, and has a finalise method which does nothing other than print to console. In Main, I create two of these objects, free one, and then create a third.

If this is compiled for 32 bit (so as to limit memory to 2 gigs), an out of memory exception is thrown when creating the third BigObject. This is because, when memory is requested for the third time, the request cannot be satisfied and so the garbage collector runs. However, the first BigObject, which is ready to be collected, has a finaliser method so instead of being collected is placed on the finalisation queue and is finalised. The garbage collecter then halts and the exception is thrown. However, if the call to GC.Collect is uncommented, or the finalise method is removed, the code will run fine.

My question is, why does the garbage collector not do everything it can to satisfy the request for memory? If it ran twice (once to finalise and again to free) the above code would work fine. Shouldn't the garbage collector continue to finalise and collect until no more memory can be free'd before throwing the exception, and is there any way to configure it to behave this way (either in code or through Visual Studio)?

Vaduz answered 6/2, 2013 at 7:56 Comment(3)
The problem is caused by the side-effect in the finalizer. Dont do that!Rhombus
I came across this a few years back too and wrote a blog post. See: xacc.wordpress.com/2011/02/22/gc-suppressfinalize (see the comments too)Rhombus
Interesting, but the code still fails if the Console.WriteLine is removed from the finaliser.Vaduz
G
2

Its undeterministic when GC will work and try to reclaim memory.

If you add this line after big1 = null . However you should be carefult about forcing GC to collect. Its not recommended unless you know what you are doing.

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

Best Practice for Forcing Garbage Collection in C#

When should I use GC.SuppressFinalize()?

Garbage collection in .NET (generations)

Gumma answered 6/2, 2013 at 8:22 Comment(4)
I understand that this is bad practice, it's just a demo app to demonstrate the problem. The question is, why doesn't the garbage collector run the finalisers and free memory (which would allow the allocation to be made) before getting to throwing the exception?Vaduz
@SeanReid #10017041 this. It should now be obvious why the GC never happened automatically. You're only creating objects on the LOH (keep in mind that int types, the way you've used them, are allocated on the stack and don't have to be collected). You are never filling up generation 0, so a GC never happens.Gumma
The thing is, a GC does run, and the free'd object's finaliser also runs. (Note the application is 32 bit, so as to ensure we run out of memory at 2Gb rather than starting to page for memory). The problem is that GC needs to run twice, once to run the finaliser and again to actually free the memory.Vaduz
that GC needs to run twice is one of the main reasons not to have a destructor (finalizer).Hancock
E
0

I guess its because the time the finalizer executes during garbage collection is undefined. Resources are not guaranteed to be released at any specific time (unless calling a Close method or a Dispose method.), also the order that finalizers are run is random so you could have a finalizer on another object waiting, while your object waits for that.

Elbaelbart answered 6/2, 2013 at 8:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.