-1

I am writing some tests to have a better understanding of how works the .NET Garbage Collector in order to build a framework without memory leak. But I am facing an unexpected behavior on my first and very simple test.

Here is my knowledge about the GC:

  • It cleans up things regularly (and when it can)
  • It cleans instances no longer referenced

Here is the little class I wrote to verify my knowledge:

public class People
{
    private People _child;
    private WeakReference<People> _parent = new WeakReference<People>(null);

    public void AddChild(People child)
    {
        _child = child;
        _child._parent.SetTarget(this);
    }
}

Basically, a parent can reference its child. Based on my knowledges above, I expect that when the parent "dies", so does its child.

The little trick here is the use of a WeakReference so the child can access to its parent but without creating a circular reference that could lead to a memory leak (that's one of the point I am trying to find out: are two instances only referencing each other garbage collected? Or in other word: do I have to use a WeakReference in this case? My guess is that they won't be garbage collected if they reference each other directly, but I actually never checked it).

Here is the little test I wrote with xUnit:

public class GCTests
{
    [Fact]
    public void TestGC()
    {
        var parent = new People();

        var weakParent = new WeakReference(parent);
        var child = new WeakReference(new People());

        parent.AddChild(child.Target as People);

        // Until now, there is a reference to the parent so the GC is not supposed to collect anything

        parent = null;

        // But now, no-one is referencing the parent so I expect the GC to collect it and removed it from memory

        // Forces the GC to collect unreferenced instances
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Assert.False(weakParent.IsAlive);
        Assert.False(child.IsAlive);
    }
}

The test fails on Assert.False(weakParent.IsAlive), meaning that someone still has a reference to the actual parent.

I also tried to use Thread.Sleep(10000); to give the GC time to collect things but it still fail on that assert.

So my question is: why are my instances not garbage collected?

  • Which one of my assertions is wrong?
  • What do I mis-understand in the process of Garbage Collection or in the use of WeakReference?

For information, the xUnit test project I am using targets .NET Core 3, but I hope it does not change anything in the GC process.

fharreau
  • 2,105
  • 1
  • 23
  • 46
  • Have you checked this [thread](https://stackoverflow.com/questions/15205891/garbage-collection-should-have-removed-object-but-weakreference-isalive-still-re)? – Pavel Anikhouski Nov 10 '19 at 18:52
  • Because you have no control on it. Calling Collect does not ensure that it will run. In general on WIndows it runs. But you can't assume that it will run. You only be sure that It runs only when memory comes near to out of. https://learn.microsoft.com/dotnet/api/system.gc.collect. When it runs, you see a lag in your app. So Collect is optimized with some factors. –  Nov 10 '19 at 19:00
  • 2
    On my machine, a console app with that code 'works' in .NET Framework 4.7.2 but fails in .NET Core 3.0. This illustrates the broad principle that you can't (and shouldn't) rely on `GC.Collect` collecting _everything_ it can. A general suggestion though, when calling `GC.Collect` is to move it into a function that calls your logic. It doesn't make it work 100% - but it makes it more likely to do what you expect it to. `WeakReference weakParent, child; YourLogicHere(out weakParent, out child); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();` – mjwills Nov 10 '19 at 20:18
  • https://stackoverflow.com/questions/15205891/garbage-collection-should-have-removed-object-but-weakreference-i alive-still-re and http://www.philosophicalgeek.com/2014/08/14/prefer-weakreferencet-to-weakreference/ may be worth a read as well. – mjwills Nov 10 '19 at 20:25
  • Lots of your assertions are wrong, but first and foremost is your assumption that circular references are a problem in the first place. It's also likely that you ran your test with the debugger attached, or on a "Debug" build, either of which can cause the GC to be less aggressive. The bottom line is, you _cannot_ ever guarantee that an unreachable object will be collected. You can make suggestions to the GC, but it's intentional design that it has its own rules for when things get collected. The only really hard and fast rule is that any reachable object will _not_ be collected. – Peter Duniho Nov 10 '19 at 23:13

1 Answers1

1

At the core of this question, you seem to want to know whether the GC can collect objects whose only reference is circular. I figured I could test this by creating two very large objects that only refer to each other, then having them go out of scope and create a third large object, just to see if I can introduce some memory pressure to try and get the GC to free stuff up. If the GC can free objects that refer to each other then the memory consumed by the program should decrease at some point. If the GC cannot, then the memory use should only increase:

using System.Threading;

namespace ConsoleApp
{
    class Program
    {

        static void Main()
        {
            Thread.Sleep(2000);

            SetupBigThings();

            Thread.Sleep(2000);

            string big = new string('a', 1000000000);


            while (true)
            {
                Thread.Sleep(2000);
            }
        }

        static void SetupBigThings()
        {
            Thread.Sleep(1000);
            BigThing x = new BigThing('x');
            Thread.Sleep(1000);
            BigThing y = new BigThing('y') { OtherBigThing = x };
            x.OtherBigThing = y;
            Thread.Sleep(1000);

        }

    }

    class BigThing
    {
        public BigThing OtherBigThing { get; set; }

        private string big;

        public BigThing(char c)
        {
            big = new string(c, 750000000);
        }
    }
}

Looking at the code we should see a memory spike at 3 seconds, then again at 4 seconds.. After 5 seconds the big objects are out of scope and maybe they will be GC'd at around 7 seconds when the next large object is created

And pretty much that's what the graph shows:

enter image description here

I thus posit that the GC can indeed collect objects whose only reference is to each other. It's probably not so naive as to simply say "which objects have 0 references?", but instead will chase out reference paths, and any objects that refer only to another node already being considered for GC are also considered GC'able. I'm no expert on the inner workings of the GC though

Caius Jard
  • 72,509
  • 5
  • 49
  • 80