Runtime: Finalizer no longer called in .NET Core 2.0 after GC.Collect / GC.WaitForPendingFinalizers

Created on 24 Nov 2017  路  6Comments  路  Source: dotnet/runtime

I already reported this issue here, but realized this repo might be more appropriate. If not, I'll close it. Otherwise I'll close the other.

Finalizer no longer called in .NET Core 2.0 after GC.Collect / GC.WaitForPendingFinalizers

As of .NET Core 2.0, an object's finalizer is no longer called when calling GC.Collect and GC.WaitForPendingFinalizers.

General

To be clear: the following code works as advertised in .NET Framework 4.7 and earlier; .NET Core 1.1 and earlier; but not in .NET Core 2.0:

[TestClass]
public class GarbageCollectionTest
{
    [TestMethod]
    [Description("Asserts that an un-rooted object's finalizer will be called.")]
    public void GarbageCollectionTest01()
    {
        // Instantiate a spy to record that the Foo finalizer is called.
        var spy = new Spy();

        // Instantiate a new Foo.
        var foo = new Foo(spy);

        // Unroot foo.
        foo = null;

        // Force a collection.
        GC.Collect(0, GCCollectionMode.Forced, blocking: true);

        // Wait for all finalizers to have executed.
        GC.WaitForPendingFinalizers();

        // Assert that Foo's finalizer is called.
        // WARNING: This assertion fails in .NET Core 2.0, but works in .NET Core 1.1 and .NET Framework 4.7 and before.
        Assert.IsTrue(spy.FinalizerIsCalled);
    }

    private class Spy
    {
        public bool FinalizerIsCalled { get; set; }
    }

    private class Foo
    {
        ~Foo()
        {
            spy.FinalizerIsCalled = true;
        }

        public Foo(Spy spy)
        {
            this.spy = spy;
        }

        private readonly Spy spy;
    }
}

Unit test output:

image

To demonstrate the issue, I created a unit test project for the following frameworks which all share the same .cs file:

  • .NET Framework 4.7 [passing]
  • .NET Core 1.0 [passing]
  • .NET Core 1.1 [passing]
  • .NET Core 2.0 [failing]

GarbageCollectionTests.zip

area-CodeGen-coreclr

Most helpful comment

It's not that the finalizer isn't called for an object that's become unreachable: it's that the object isn't becoming unreachable and thus isn't collected and isn't finalized. The lifetime of the object is getting extended in the method, with a temporary keeping it alive. Try creating the object instead in a non-inlined helper.

All 6 comments

The behavior you are seeing is not a bug. https://github.com/dotnet/coreclr/issues/5826#issuecomment-226574611 explains the details.

Duplicate of dotnet/runtime#6157

Interesting. I read your comment and the one thereafter, but I'm still not sure I completely understand why my finalizer is never called in .NET Core 2.0 when running in Debug mode (but it does get called in Release mode which I hadn't realized until reading that thread), but so this is what I gathered as to why my test is failing on .NET Core 2.0 in Debug mode:

  • The exact time when a finalizer is called is undefined, even after calling GC.Collect and GC.WaitForPendingFinalizers.
  • RuyJIT replaces JIT32 as of .NET Core 2.0, which has a more aggressive inlining mode and would explain the difference in behavior.

What eludes me is how come the finalizer in my test is never called, even when I keep the program alive (by awaiting another thread that performs an infinite loop). Could the answer lie in the way the JIT emits the assembly language output in debug mode?

It's not that the finalizer isn't called for an object that's become unreachable: it's that the object isn't becoming unreachable and thus isn't collected and isn't finalized. The lifetime of the object is getting extended in the method, with a temporary keeping it alive. Try creating the object instead in a non-inlined helper.

That makes a lot of sense. However I'm unsure how to create an object from a non-inlined helper. I mean, I thought I knew I did, but when I try it still fails when built in Debug mode.

Here's what I tried:

[TestMethod]
[Description("Asserts that an un-rooted object graph will be garbage collected.")]
public void GarbageCollectionTest01()
{
    var spy = new Spy();
    CreateFoo(spy);
    GC.Collect(0, GCCollectionMode.Forced, blocking: true);
    GC.WaitForPendingFinalizers();

    Thread.Sleep(10);

    Assert.IsTrue(spy.FinalizerIsCalled);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private Foo CreateFoo(Spy spy)
{
    return new Foo(spy);
}

@sfmskywalker, my point of suggesting a non-inlined helper was to keep the object out of the frame in which you're then forcing a collection. The moment you return the object from the helper into the calling frame, you potentially enable the JIT to extend the lifetime of the object past that garbage collection.

What is it you're trying to do?

Now I see. You're right; when I instantiate Foo in the non-inlined helper without returning it to the caller, its finalizer does get called.

This small unit test originated from a slightly different one in one of my projects where I wanted to test whether the GC would reclaim an object even if there would be a circular reference between Foo and Bar. When this unit test started failing after upgrading to .NET Core 2.0, I started bringing that test down to its bare minimum to find the root cause of the issue.

But, as you and @jkotas point out, the problem was my incorrect assumption about how things were supposed to work.

Thank you both for helping me get a much better understanding, much appreciated!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

btecu picture btecu  路  3Comments

nalywa picture nalywa  路  3Comments

omajid picture omajid  路  3Comments

iCodeWebApps picture iCodeWebApps  路  3Comments

GitAntoinee picture GitAntoinee  路  3Comments