WeakReference not Garbage Collected

110 views Asked by At

It is my understanding that after all strong references to the Target of a WeakReference are set to null and the GC is invoked, that weak reference should no longer be alive.

However, the code below does not appear to follow this expectation:

    static void Main(string[] _) {
        var person = new Person();
        var wr = new WeakReference(person);

        person = null;

        // We should have access to the person here
        if (wr.Target is Person shouldExist)
        {
            Console.WriteLine($"Person exists! IsAlive: {wr.IsAlive}.");
            shouldExist = null;
        }
        else
        {
            Console.WriteLine($"Person does not exist :( IsAlive: {wr.IsAlive}.");
        }

        // Invoke GC.Collect.            
        GC.Collect();


        if (wr.Target is Person shouldNotExist)
        {
            Console.WriteLine("This person should have been garbage collected");
            Console.WriteLine($"IsAlive: {wr.IsAlive}");
        }
        else
        {
            Console.WriteLine("This person was garbage collected and weak reference is no longer alive");
            Console.WriteLine($"IsAlive: {wr.IsAlive}");
        }
    }

where

class Person
{
    private int mI = 3;
    public int MI { get => mI; set => mI = value; }
}

And the output is

Person exists! IsAlive: True. This person should have been garbage collected IsAlive: True

I was expecting the output to be:

Person exists! IsAlive: True. This person was garbage collected and weak reference is no longer alive IsAlive: False

Am I missing something here about how weak references work?

3

There are 3 answers

0
Matthew Watson On BEST ANSWER

The use of the references to the Person object in Main() is keeping the object alive until the end of the method.

If you change the code as follows (so that the access to the Person object is in a separate method), it will work as expected in C# 12/.NET 8:

public static class Program                                    
{
    static void Main()
    {
        var wr = getWeakReference();
        checkPersonExists(wr);
        GC.Collect();

        if (wr.Target is Person shouldNotExist)
        {
            Console.WriteLine("This person should have been garbage collected");
            Console.WriteLine($"IsAlive: {wr.IsAlive}");
        }
        else
        {
            Console.WriteLine("This person was garbage collected and weak reference is no longer alive");
            Console.WriteLine($"IsAlive: {wr.IsAlive}");
        }
    }

    static WeakReference getWeakReference()
    {
        var person = new Person();
        var wr     = new WeakReference(person);

        return wr;
    }

    static void checkPersonExists(WeakReference wr)
    {
        // We should have access to the person here
        if (wr.Target is Person)
        {
            Console.WriteLine($"Person exists! IsAlive: {wr.IsAlive}.");
        }
        else
        {
            Console.WriteLine($"Person does not exist :( IsAlive: {wr.IsAlive}.");
        }

    }
}

class Person
{
    private int mI = 3;
    public  int MI { get => mI; set => mI = value; }
}

NOTE: This behaviour is not guaranteed and may vary in different versions of C#/.NET.

2
freakish On

From the C# standard:

7.9 Automatic memory management

...

Note: The C# compiler and the garbage collector might choose to analyze code to determine which references to an object might be used in the future. For instance, if a local variable that is in scope is the only existing reference to an object, but that local variable is never referred to in any possible continuation of execution from the current execution point in the procedure, the garbage collector might (but is not required to) treat the object as no longer in use. end note

In practice C# runtimes will keep local references until the method exits, in order to enable debugging. To properly test weak references you need to construct Person and its WeakReference in a separate method.

0
Guru Stron On

Can't find actual docs which will explain the behavior but this is quite delicate part of the compiler/JIT behavior in terms how long it will preserve objects/links in some cases. "Fixing" null checks and moving initialization to a separate method (i.e. removing references to the Person inside the Main method) makes the code behave as expected for me (.NET 8):

var wr = Initialize();

// We should have access to the person here
if (wr.Target is not null)
{
    Console.WriteLine($"Person exists! IsAlive: {wr.IsAlive}.");
}
else
{
    Console.WriteLine($"Person does not exist :( IsAlive: {wr.IsAlive}.");
}

// Invoke GC.Collect.            
GC.Collect();


if (wr.Target is not null)
{
    Console.WriteLine("This person should have been garbage collected");
    Console.WriteLine($"IsAlive: {wr.IsAlive}");
}
else
{
    Console.WriteLine("This person was garbage collected and weak reference is no longer alive");
    Console.WriteLine($"IsAlive: {wr.IsAlive}");
}

WeakReference Initialize()
{
    var person = new Person();
    var weakReference = new WeakReference(person);

    person = null;
    return weakReference;
}

Demo @ sharplab

NB!

Be sure to run this in Release mode since JIT can prolong object liftime in Debug - see this answer.