2

In my program I have a service object which keeps weak references to objects. I have an automated test that asserts that when all references to the object are removed, the weak reference becomes freed up. The purpose of the test is to prove that the service won't cause objects to remain in memory when there is no reason for them to be kept around.

Here is a simplified version of that test:

[Test]
public void WeakReference()
{
    var o = Allocate();
    var reference = new WeakReference(o.Value);
    Assert.IsTrue(reference.IsAlive);

    o.Value = null;
    GC.Collect();
    Assert.IsFalse(reference.IsAlive);
}

private MyObject Allocate()
{
    return new MyObject
    {
        Value = new object()
    };
}

private class MyObject
{
    public object Value { get; set; }
}

In a .NET 4.8 NUnit test project this test passes. However, on the current branch of the project, I have the test inside of a .NET 5 project. In that project, this same test code fails because the weak reference remains "alive".

I also tried by changing the GC.Collect() call to

GC.Collect(int.MaxValue, GCCollectionMode.Forced, blocking: true);

No dice.

Has there been a fundamental change in the way the garbage collection works in .NET 5 that makes the above code no longer work? How can I still prove that my service object is not holding onto objects in memory?

bgh
  • 1,986
  • 1
  • 27
  • 36

1 Answers1

2

I was able to cause your test to pass under these 3 conditions:

  1. Loop over GC.Collect() while IsAlive.
  2. Compiler optimizations are ON.
  3. The debugger is not attached.

This modified test code demonstrates the looping mentioned in #1 above and repeats the test a significant number of times. It always passes for me.

[Test, Repeat(1000)]
public void WeakReference()
{
    var o = Allocate();
    var reference = new WeakReference(o.Value);
    Assert.IsTrue(reference.IsAlive);

    o.Value = null;

    for (var i = 0; i < 2 && reference.IsAlive; i++)
        GC.Collect();

    Assert.IsFalse(reference.IsAlive);
}

I'm not yet knowledgeable enough to say why this works exactly, but I've gathered that it has to do with the conditions that will cause an object reference to be a GC root (always reachable & not collectable by GC).

Ronnie Overby
  • 45,287
  • 73
  • 267
  • 346
  • Brilliant! Interestingly, with all the conditions above met, it is not even necessary to set o.Value to null. It will still be garbage collected because it is not referenced again. – bgh Aug 25 '21 at 05:21
  • Whereas the above works, enabling compiler optimizations on our test project is not really a option, since then the tests cannot be debugged. The breakpoints will not hit. – bgh Aug 25 '21 at 05:45
  • @bgh Correct. I don't know of a way around that. – Ronnie Overby Aug 26 '21 at 15:57
  • GC operates differently under the debugger. See: https://stackoverflow.com/a/7165380/1608430 – voidp Oct 29 '22 at 00:11