Anonymous delegate in WeakReference never be collected?

using UnityEngine;
using System;
using System.Collections;

public class Test : MonoBehaviour
{
    WeakReference wr = null;
    private void Start()
    {
        wr = new WeakReference(new Action(()=> { }));
        StartCoroutine(Check());
    }

    IEnumerator Check()
    {
        int count = 0;
        while (true)
        {
            print("wr.Target == null ? " + (wr.Target == null ? "Yes" : "No"));
            yield return new WaitForSeconds(1f);
            count++;
            if ((count+1) % 3 == 0)
            {
                print("!GC!");
                System.GC.Collect();
                count -= 3;
            }
        }
    }
}

The output is always:

wr.Target == null ? No

Why is that? Thanks!

Your anonymous method is translated to a private static member method of your class. For optimisations the compiler creates a hidden static cache field for static anonymous method references since they will never change. If you look at the generated IL code you will find a field like this:

.field private static class [System.Core]System.Action '<>f__am$cache0'

At first encounter of the line where the System.Action is created it will store the reference in the static variable. Therefore there’s always a strong reference to that delegate.

If your anonymous method would be a closure it would turn the method into an instance member of a closure class. This delegate can not be cached and therefore can be collected.

For example:

private void Start()
{
    int val = 3;
    wr = new WeakReference(new Action(()=> { Debug.Log("Value: " + val); }));
    StartCoroutine(Check());
}

In this case the delegate will be collected since the method is an instance method of an object and not a static method.

Interestingly this caching optimisation only applies to anonymous (static) methods and not to “normal” static method

private void Start()
{
    wr = new WeakReference(new Action(SomeMethod));
    StartCoroutine(Check());
}

private static SomeMethod()
{
    
}

This will be collected. However apart from this background information, weak references shouldn’t be used in such a case. Read the guildlines at the end of this page.

Avoid using weak references to small
objects because the pointer itself may
be as large or larger.

Avoid using weak references as an
automatic solution to memory
management problems. Instead, develop
an effective caching policy for
handling your application’s objects.


Just for reference here’s the IL code of your Start method:

IL_0000: ldarg.0
IL_0001: ldsfld class [System.Core]System.Action WeakRefTest::'<>f__am$cache0'
IL_0006: brtrue.s IL_0019

IL_0008: ldnull
IL_0009: ldftn void WeakRefTest::'<Start>m__0'()
IL_000f: newobj instance void [System.Core]System.Action::.ctor(object, native int)
IL_0014: stsfld class [System.Core]System.Action WeakRefTest::'<>f__am$cache0'

IL_0019: ldsfld class [System.Core]System.Action WeakRefTest::'<>f__am$cache0'
IL_001e: newobj instance void [mscorlib]System.WeakReference::.ctor(object)
IL_0023: stfld class [mscorlib]System.WeakReference WeakRefTest::wr
IL_0028: ldarg.0
IL_0029: ldarg.0
IL_002a: call instance class [mscorlib]System.Collections.IEnumerator WeakRefTest::Check()
IL_002f: call instance class [UnityEngine]UnityEngine.Coroutine [UnityEngine]UnityEngine.MonoBehaviour::StartCoroutine(class [mscorlib]System.Collections.IEnumerator)
IL_0034: pop
IL_0035: ret

The first section loads the cache field and the second creates the Action instance if the cache field is null. To avoid confusion my class is called “WeakRefTest” and the anonymous method that the compiler generated is called '<Start>m__0'