runtime: AssemblyLoadContext never get unloaded if there's a dynamic operation

Description

There’s an assembly with below code :

   dynamic eo = new ExpandoObject();
   eo.abc = 123; // this line will cause the issue

If I load the assembly in the AssemblyLoadContext with isCollectible = true, than this load context will never get unloaded via below methods:

loadContext.Unload();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

I think there’re some dynamic generated objects still reference the load context. But I can’t find anyway to release them.

Reproduction Steps

Just put the below code in a console app (.Net 5 or 6), and add microsoft.codeanalysis.csharp.scripting package will re-produce the issue.
Then you can find the output of the count always increase.

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

public class Program
{
    public static async Task Main(string[] args)
    {
       // ==== prepare the assembly ====
        string code = @"dynamic eo = new ExpandoObject(); 
                        eo.abc = 123; // comment out this line will fix the issue
                        Console.WriteLine(System.AppDomain.CurrentDomain.GetAssemblies().Count());";
        ScriptOptions so = ScriptOptions.Default
            .AddImports("System", "System.Linq", "System.Dynamic")
            .AddReferences("System", "System.Core", "Microsoft.CSharp");

        var cs = CSharpScript.Create(code, so);
        var compilation = cs.GetCompilation();
        using MemoryStream ms = new MemoryStream();
        var rslt = compilation.Emit(ms);
        // ==== finish preparing ====

        while (true)
        {
            ms.Seek(0, SeekOrigin.Begin);

            AssemblyLoadContext lc = new AssemblyLoadContext("test", isCollectible: true);
            var ass = lc.LoadFromStream(ms);

            var typ = ass.GetType("Submission#0");
            var mem = typ.GetMethod("<Factory>", BindingFlags.Static | BindingFlags.Public);

            var retTask = mem.Invoke(null, new object[] { new object[2] }) as Task<object>;
            var rsltTsk = await retTask;

            lc.Unload();

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

            await Task.Delay(200);
        }
    }
}

Expected behavior

The output of the count will only increase once for the first time.

Actual behavior

The output of the count always increase.

Regression?

No response

Known Workarounds

No response

Configuration

Which version of .NET is the code running on? .Net 5、.Net 6

What OS and version, and what distro if applicable? Only Tested in Win 10 Pro.

What is the architecture (x64, x86, ARM, ARM64)? x64

Do you know whether it is specific to that configuration? No

Other information

No response

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Comments: 15 (6 by maintainers)

Commits related to this issue

Most upvoted comments

I think I found the source of the rooting, it’s in a cache in Microsoft.CSharp.RuntimeBinder. https://github.com/dotnet/runtime/blob/c30866d60c49a19e66e8edcc3a0876585fe760fc/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/BinderEquivalence.cs#L32-L33

I’m not sure whether it will be fixed; this is an archived component.

If you don’t load the Microsoft.CSharp.dll into your plugin ALC there will be something keeping the references alive as it will use the assembly loaded into the Default ALC Most likely a similar reason to this: #13283

So there is a small memory hit here as each plugin will load the assembly separately instead of sharing.

Doing so can have a noticeable performance impact, jit time increased significantly。

I will work on this soon.

I think I found the source of the rooting, it’s in a cache in Microsoft.CSharp.RuntimeBinder.

https://github.com/dotnet/runtime/blob/c30866d60c49a19e66e8edcc3a0876585fe760fc/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/BinderEquivalence.cs#L32-L33

I’m not sure whether it will be fixed; this is an archived component.

If you don’t load the Microsoft.CSharp.dll into your plugin ALC there will be something keeping the references alive as it will use the assembly loaded into the Default ALC Most likely a similar reason to this: https://github.com/dotnet/runtime/issues/13283

So there is a small memory hit here as each plugin will load the assembly separately instead of sharing.