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
- issue https://github.com/dotnet/runtime/issues/71629 — committed to stg609/Tutorials by deleted user 2 years ago
Doing so can have a noticeable performance impact, jit time increased significantly。
I will work on this soon.
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.