Skip to content

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

Closed

Description

@sfmskywalker

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

Activity

  1. jkotas commented on Nov 24, 2017

    @jkotas
    Member

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

    Duplicate of #6157

  2. sfmskywalker commented on Nov 25, 2017

    @sfmskywalker
    Author

    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?

  3. stephentoub commented on Nov 25, 2017

    @stephentoub
    Member

    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.

  4. sfmskywalker commented on Nov 25, 2017

    @sfmskywalker
    Author

    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);
    }
  5. stephentoub commented on Nov 25, 2017

    @stephentoub
    Member

    @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?

  6. sfmskywalker commented on Nov 25, 2017

    @sfmskywalker
    Author

    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!

  7. transferred this issue fromdotnet/coreclron Jan 31, 2020
  8. ghost locked as resolved and limited conversation to collaborators on Dec 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions