Search code examples
c#weak-references.net-core-3.1.net-4.8

WeakReference behaves differently between .Net Framework and .Net Core


Consider the following code:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

#nullable enable

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            var list    = makeList();
            var weakRef = new WeakReference(list[0]);

            list[0] = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine(weakRef.IsAlive);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        static List<int[]?> makeList()
        {
            return new List<int[]?> { new int[2] };
        }
    }
}
  • With either a release or a debug build on .Net Framework 4.8, that code prints False.
  • With either a release or a debug build on .Net Core 3.1, that code prints True.

What is causing this difference in behaviour? (It's causing some of our unit tests to fail.)

Note: I put the list initialisation into makeList() and turned off inlining in an attempt to make the .Net Core version work the same as the .Net Framework version, but to no avail.


[EDIT] As Hans pointed out, adding a loop fixes it.

The following code will print False:

var list    = makeList();
var weakRef = new WeakReference(list[0]);

list[0] = null;

for (int i = 0; i < 1; ++i)
    GC.Collect();

Console.WriteLine(weakRef.IsAlive);

But this will print True:

var list    = makeList();
var weakRef = new WeakReference(list[0]);

list[0] = null;

GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();

// Doesn't seem to matter how many GC.Collect() calls you do.

Console.WriteLine(weakRef.IsAlive);

This has got to be some kind of weird Jitter thing...


Solution

  • Just because something is allowed to be collected doesn't mean it's obligated to be collected as soon as is possible. While the language states that the GC is allowed to determine that a local variable is never read again, and thus not consider it a root, that doesn't mean you can rely on a local variable's contents being collected immediately after you last read from it.

    This isn't some change between defined behavior in the runtime, this is undefined behavior in both runtimes, so differences between them is entirely acceptable.