Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
sfmskywalker opened this issue Nov 24, 2017 · 6 comments
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Comments

@sfmskywalker
Copy link

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

@jkotas
Copy link
Member

jkotas commented Nov 24, 2017

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

Duplicate of #6157

@jkotas jkotas closed this as completed Nov 24, 2017
@sfmskywalker
Copy link
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?

@stephentoub
Copy link
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.

@sfmskywalker
Copy link
Author

sfmskywalker commented Nov 25, 2017

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);
}

@stephentoub
Copy link
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?

@sfmskywalker
Copy link
Author

sfmskywalker commented Nov 25, 2017

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!

@msftgits msftgits transferred this issue from dotnet/coreclr Jan 31, 2020
@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 19, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

No branches or pull requests

3 participants