Runtime: GC.Collect: Object without reference will be collected in .NET Framework but will NOT been collected in .NET Core

Created on 12 May 2020  路  10Comments  路  Source: dotnet/runtime

I've searched this issue in these keywords (GC framework core not collected) but can't find any similar results. So I post it here.

area-GC-coreclr @Maoni0

Issue

If an object is created in a method but never been referenced, the GC will collect it in .NET Framework but will not collect it in .NET Core.

  • Tested .NET Framework version: net45 / net48
  • Tested .NET Core version: 3.1.201

Question

Is the difference a normal feature or is it a bug in .NET Core?

Minimal Example

See this code below:

  1. We create a weak reference to wrap any instance without any direct strong reference.
  2. We do a GC.Collect() to try to collect the object.
class Program
{
    static void Main(string[] args)
    {
        new WeakReference<Foo>(new Foo());
        GCTest();
    }

    private static void GCTest()
    {
        while (true)
        {
            Thread.Sleep(500);
            GC.Collect();
        }
    }
}

public class Foo
{
    ~Foo()
    {
        Console.WriteLine("Foo is collected");
    }
}

Run this code in net48 we'll get the output below:

Foo is collected

But if we run this code in netcoreapp3.1 we'll get nothing:


This means that the Foo object is only been collected in .NET 4.8 but is not been collected in .NET Core 3.1.

Extra Info

Change the creation of the Foo instance into a method and the object will be collected in both .NET Framework and .NET Core.

    static void Main(string[] args)
    {
--      new WeakReference<F1>(new F1());
++      NewObject();
        GCTest();
    }

++  [MethodImpl(MethodImplOptions.NoInlining)]
++  private static object NewObject() => new WeakReference<F1>(new F1());

    private static void GCTest()
    {
        while (true)
        {
            Thread.Sleep(500);
            GC.Collect();
        }
    }

EDIT: Thanks to @janvorli and I've added the NoInlining attribute on the NewObject method.

area-CodeGen-coreclr

Most helpful comment

I don't see a bug here.

The behavior change between .Net Framework 4.8 and .Net Core 3.1 is from tiered compilation.

In .Net Core 3.1 the method is initially jitted without optimization and when that happens GC lifetimes in methods are "untracked" and so don't end until the end of the method. Because of this the object is kept alive as long as Main is active.

If you disable tiered compilation (via COMPlus_TieredCompilation=0 or equivalent) then .Net Core 3.1 collects the object.

For the most part, in optimized code, objects become collectible once the code in the method passes the last possible reference point for the object. But untracked gc references can also appear in optimized code in some cases. So the only guarantee we can provide is that references to an object from a given call to a method A will expire once A returns. If A is inlined into some other method B then references from A may extend until B returns.

The transformation above to hold references in a noinline method is the simplest way to ensure object references expire at known places in code.

All 10 comments

I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label.

I've tried to decompile the code and get the IL below:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Header Size: 1 byte
    // Code Size: 17 (0x11) bytes
    .maxstack 8
    .entrypoint

    /* (12,13)-(12,45) Program.cs */
    /* 0x00000251 7305000006   */ IL_0000: newobj    instance void Walterlv.Demo.Weak.Foo::.ctor()
    /* 0x00000256 730C00000A   */ IL_0005: newobj    instance void class [System.Runtime]System.WeakReference`1<class Walterlv.Demo.Weak.Foo>::.ctor(!0)
    /* 0x0000025B 26           */ IL_000A: pop
    /* (14,13)-(14,22) Program.cs */
    /* 0x0000025C 2802000006   */ IL_000B: call      void Walterlv.Demo.Weak.Program::GCTest()
    /* (15,9)-(15,10) Program.cs */
    /* 0x00000261 2A           */ IL_0010: ret
} // end of method Program::Main

The IL codes of .NET Core version and .NET Framework version are almost the same, so I guess this is an issue of the CLR.

I've noticed that there is a pop in the IL, so the local variable created by the newobj should have been collected.

I've noticed that there is a pop in the IL, so the local variable created by the newobj should have been collected.

A pop won't collect an object, it just kicks it off the managed execution stack.

Tagging subscribers to this area: @Maoni0
Notify danmosemsft if you want to be subscribed.

Whether it will be collected or not depends on the lifetime of the local that stores the Foo instance. Even if you don't assign it to anything, JIT may create a local on stack or keep it in registers with a lifetime that covers all the Main method. Whether it does that or not depends on various factors and is not defined in any way. If you want to make sure an instance goes away, I would recommend putting it into a separate method marked with [MethodImpl(MethodImplOptions.NoInlining)] so that JIT cannot inline it into the caller.

Here is a transformation of your example that should behave as you wanted in all frameworks:
```c#
using System.Runtime.CompilerServices;
class Program
{
[MethodImpl(MethodImplOptions.NoInlining)]
static void Test()
{
new WeakReference(new Foo());
}

static void Main(string[] args)
{
    Test();
    GCTest();
}

private static void GCTest()
{
    while (true)
    {
        Thread.Sleep(500);
        GC.Collect();
    }
}

}

public class Foo
{
~Foo()
{
Console.WriteLine("Foo is collected");
}
}
```

I don't see a bug here.

The behavior change between .Net Framework 4.8 and .Net Core 3.1 is from tiered compilation.

In .Net Core 3.1 the method is initially jitted without optimization and when that happens GC lifetimes in methods are "untracked" and so don't end until the end of the method. Because of this the object is kept alive as long as Main is active.

If you disable tiered compilation (via COMPlus_TieredCompilation=0 or equivalent) then .Net Core 3.1 collects the object.

For the most part, in optimized code, objects become collectible once the code in the method passes the last possible reference point for the object. But untracked gc references can also appear in optimized code in some cases. So the only guarantee we can provide is that references to an object from a given call to a method A will expire once A returns. If A is inlined into some other method B then references from A may extend until B returns.

The transformation above to hold references in a noinline method is the simplest way to ensure object references expire at known places in code.

@AndyAyersMS Thank you for your information about tiered compilation.

I just test my project by adding the disable of the tired compilation and the object is suddenly been collected.

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net48;netcoreapp3.1</TargetFrameworks>
+   <TieredCompilation>false</TieredCompilation>
  </PropertyGroup>

@walterlv any other concerns here? If not, let's close this issue.

@AndyAyersMS Everything can be explained, so this issue can be closed.

@walterlv thank you for asking about this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

omajid picture omajid  路  3Comments

noahfalk picture noahfalk  路  3Comments

aggieben picture aggieben  路  3Comments

yahorsi picture yahorsi  路  3Comments

chunseoklee picture chunseoklee  路  3Comments