Understanding garbage collector behavior for a loc

2019-04-11 10:22发布

问题:

Below is a very simple console app (try the fiddle):

using System;
using System.Threading;
using System.Threading.Tasks;

public class ConsoleApp
{
    class Callback
    {
        public Callback() { }
        ~Callback() { Console.WriteLine("~Callback"); }
    }

    static void Test(CancellationToken token)
    {
        Callback callback = new Callback();

        while (true)
        {
            token.ThrowIfCancellationRequested();

            // for the GC
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();

            Thread.Sleep(100);
        }

        // no need for KeepAlive?
        // GC.KeepAlive(callback);      
    }

    public static void Main()
    {
        var cts = new CancellationTokenSource(3000);
        try
        {
            Test(cts.Token);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Enter to exit...");
        Console.ReadLine();
    }
}

Here, the callback object doesn't get garbage-collected until it goes out of scope of the Test method.

I thought GC.KeepAlive(callback) would be required to keep it alive inside Test (as MSDN suggests), but apparently it is not (commented out in the code above).

Now if I change the code like below, the callback does get garbage-collected, as expected:

Callback callback = new Callback();
callback = null;

This happens with .NET 4.5.1.

The question: Am I missing something? Can I rely upon this behavior, or is it something .NET version specific?

回答1:

@Porges' comments explain everything very well:

Try building & running it in Release mode, without the debugger attached. I get the expected behaviour there but not in Debug.
...
ie. running with Ctrl-F5, not just F5. It collects it instantly for me in each of .NET 4/4.5/4.5.1. But yes, you can't really rely on this behaviour.

The Release build and Ctrl-F5 brought back the expected behavior. I urge @Porges to post this as an answer, which I'd up-vote and accept with thanks.

As a follow-up, I'd like to feature the following interesting behavior. Now with Release + Ctrl-F5, even if I un-comment the // GC.KeepAlive(callback) line in my code, the callback still gets garbage-collected. Apparently, this is because the compiler recognizes this line as unreachable due to while (true) loop and still doesn't emit a strong reference on callback.

The following is the correct pattern:

static void Test(CancellationToken token)
{
    Callback callback = new Callback();

    try
    {
        while (true)
        {
            token.ThrowIfCancellationRequested();

            // for the GC
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();

            Thread.Sleep(100);
        }
    }
    finally
    {
        GC.KeepAlive(callback);     
    }
}

It's also interesting to look at the GC.KeepAlive implementation:

[MethodImpl(MethodImplOptions.NoInlining)]
public static void KeepAlive(object obj)
{
}

As expected, it does nothing and merely servers as a hint to the compiler to generate IL code which keeps a strong reference to the object, up to the point where KeepAlive is called. MethodImplOptions.NoInlining is very relevant here to prevent any optimizations like above.



回答2:

.NET garbage collection is non-deterministic.

The MSDN page to which you linked says it all really - emphasis added:

The purpose of the KeepAlive method is to ensure the existence of a reference to an object that is at risk of being prematurely reclaimed by the garbage collector.

Just because callback can be garbage collected before it goes out of scope on return from Test does not mean that it will be.