runtime: GC does not collect all objects
Description
GC doesn’t collect objects, that are declared in the same method, but the reference to them is gone
Reproduction Steps
Run this code:
using System;
class A
{
public static int InstanceCount;
public A()
{
InstanceCount++;
}
~A()
{
InstanceCount--;
}
}
public class Program
{
public static void Main(string[] args)
{
var a = new A();
a = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(A.InstanceCount);
}
}
I Because we nulled Debug
mode it might be fine to not collect a
because we might want to check its value after GC.Collect()
and GC.WaitForPendingFinalizers()
.a
before we are not able to track it anymore even in debug mode, right? However, it still output 1
even in Release
mode for me
Expected behavior
GC.Gollect()
should collect object a
, because the reference to it is already gone from the stack. GC.WaitForPendingFinalizers()
should wait for its finalizer to be called, so the output value of the InstanceCount
field to the console must be 0
Actual behavior
Seems like GC.Gollect()
doesn’t mark a
as unused and doesn’t collect it. So we get 1
displayed in the console
Regression?
The behaviour is expected in sharplab in Release
mode, I don’t know what version of runtime it is currently using: https://sharplab.io/#v2:C4LgTgrgdgPgAgJgIwFgBQ7EAICC6De6AkHAMxZxIBsWAllMFgJJQDOwAhlAMYCmAwgHtowANzpiZXAAoAlMUJoiRFuy58hIgNRbxSgL4SlAPxxyFxFW048BwhgFoHeoobRvM5bJQDsBdFiBFF7UFAAsWACyHPTSlAAMANoAulgcYADmrPJKikH5WABu6WlYALxYULwA7jKyegVBHOWVEAA2bQ2NWADi/AB0Qh283MByLkR9/QDqMcAAYoJgAAq8UAAm9Bnz9BxttABevGCs45aUAJzSOP2qNhr2wPXEbvpAA===
Known Workarounds
No response
Configuration
Tried on runtimes: net472
(reproduces only in Debug
mode), net6.0
v6.0.9, net7.0
v7.0.0-rc.1.22427.1
Tried on OSes: Win 11 21H2 build 22000.1042 and Win 11 22H2 build 22621.521
Architecture: x64
Other information
No response
category:correctness theme:gc-info skill-level:intermediate cost:small impact:medium
About this issue
- Original URL
- State: open
- Created 2 years ago
- Comments: 17 (17 by maintainers)
At the IL level the C# compiler generates:
so there isn’t actually a local at all.
Right, we plan some improvements on codegen level for this, namely we want to be careful inlining methods with pinvokes/fcalls inside
@DoctorKrolic Anyways, regardless of changes to GC, if you’re wanting deterministic cleanup of something you should actually be implementing the
Dispose()
pattern, not relying on finalizers (which have multiple reasons they might not be cleaned up when you expect).I feel like it happens because
GC.Collect/WaitForPendingFinalizers
are inlined and explode the caller with gc transition machinery because of pinvokes/fcalls (untrackable locals?). This works:We might want to slap
[MethodImpl(MethodImplOptions.NoInlining)]
to such GC helpers