【问题标题】:Why does AppDomain still have a reference to a dynamically loaded assembly after the unload event fires?为什么在 unload 事件触发后 AppDomain 仍然引用动态加载的程序集?
【发布时间】:2021-05-11 16:00:21
【问题描述】:

这是一个 netcore 问题,而不是 NETFX 问题。

此检查演示了该行为。您需要提供一个程序集以供其加载并使代码中的名称与文件系统中的名称匹配。

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;

namespace unload
{
  public class ALC : AssemblyLoadContext
  {
    public ALC() : base(isCollectible: true) { }
    protected override Assembly Load(AssemblyName name) => null;
  }
  class Program
  {
    static void Main(string[] args)
    {
      var alc1 = new ALC();
      var alc2 = new ALC();
      Assembly assy1, assy2;
      using (var stream = new FileStream("./assy.dll", FileMode.Open))
        assy1 = alc1.LoadFromStream(stream);
      using (var stream = new FileStream("./assy.dll", FileMode.Open))
        assy2 = alc2.LoadFromStream(stream);
      var currentDomain = AppDomain.CurrentDomain;
      alc1.Unloading += alc =>
      {
        Console.WriteLine("alc1 HAS UNLOADED");
      };
      alc2.Unloading += alc =>
      {
        Console.WriteLine("alc2 HAS UNLOADED");
      };
      alc1.Unload();
      assy1 = null;
      alc1 = null;
      alc2.Unload();
      assy2 = null;
      alc2 = null;
      GC.Collect(); //no refs and force GC
      Thread.Sleep(100); // let other things complete
      var duplicates = from a in currentDomain.GetAssemblies() group a by a.FullName into g where g.Count() > 1 select g;
      while (duplicates.Any())
      {
        GC.Collect();
        Thread.Sleep(500);
        duplicates = from a in currentDomain.GetAssemblies() group a by a.FullName into g where g.Count() > 1 select g;
        Console.WriteLine(string.Join(", ", duplicates.Select(g => $"{g.Count()} {g.Key}")));
      }
    }
  }
}

如果您运行此程序,您将看到卸载事件触发,但 currentDomain.GetAssemblies() 继续找到程序集的两个实例。

AssemblyLoadContext.Unload() 的文档说

  • 只有可收集的 AssemblyLoadContext 才能被卸载。
  • 卸载将异步进行。
  • 存在对 AssemblyLoadContext 的引用时不会发生卸载

我相信这个例子符合所有这些要求。还有什么可能干扰?这有什么已知的错误吗?

是否有可能实际卸载了程序集但 currentDomain 未能更新其簿记?你会怎么看?

【问题讨论】:

  • 这是一个Unloading 事件,而不是Unloaded,它可能还没有完全卸载。在您正在卸载的函数之外尝试GC.Collect(2); GC.WaitForPendingFinalizers(); GC.Collect(2);
  • @Charlieface 看到我的自我回答。打扰发布版本的有力论据!性能开销从来没有让我担心,但内存泄漏是另一回事。
  • stackoverflow.com/questions/755680/… 这就是为什么我说在函数之外

标签: c# .net-core assembly-loading


【解决方案1】:

灵机一动,我尝试了一个发布版本,问题就消失了。但为什么?评论者 Charlieface 提供了解释原因的信息以及指向另一个问题的链接。他是对的,但我认为两者之间的关系并不明显,所以我将在这里拼写出来。

问题实际上出在我的测试设计上,特别是整个测试都在一个方法体中。在调试构建中,局部变量未优化。在方法退出之前不会释放引用,并且当方法仍然可以看到重复的程序集时不会退出。

像这样重构

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;

namespace unload
{
  class Program
  {
    static void Main(string[] args)
    {
      LoadAndUnloadTest();
      GC.Collect(2);
      var currentDomain = AppDomain.CurrentDomain;
      var duplicates = from a in currentDomain.GetAssemblies() group a by a.FullName into g where g.Count() > 1 select g;
      while (duplicates.Any())
      {
        Thread.Sleep(500);
        duplicates = from a in currentDomain.GetAssemblies() group a by a.FullName into g where g.Count() > 1 select g;
        Console.WriteLine(string.Join(", ", duplicates.Select(g => $"{g.Count()} {g.Key}")));
      }
    }
    static void LoadAndUnloadTest()
    {
      var alc1 = new ALC();
      var alc2 = new ALC();
      Assembly assy1, assy2;
      using (var stream = new FileStream("./assy.dll", FileMode.Open))
        assy1 = alc1.LoadFromStream(stream);
      using (var stream = new FileStream("./assy.dll", FileMode.Open))
        assy2 = alc2.LoadFromStream(stream);
      alc1.Unloading += alc =>
      {
        Console.WriteLine("AFTER UNLOAD alc1");
      };
      alc2.Unloading += alc =>
      {
        Console.WriteLine("AFTER UNLOAD alc2");
      };
      alc1.Unload();
      alc2.Unload();
    }
  }
  public class ALC : AssemblyLoadContext
  {
    public ALC() : base(isCollectible: true) { }
    protected override Assembly Load(AssemblyName name) => null;

  }
}

我们有完美的行为在调试版本

请注意,需要显式执行垃圾回收。为什么 GC 不会最终自行发生?在更大的应用程序中,它会执行更多操作,但在此测试的封闭范围内不会创建更多对象,因此不会达到内存压力等的触发阈值。

通常是什么触发它?答案就在这里:When does garbage collection get triggered in C#?

【讨论】:

    猜你喜欢
    • 2021-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-11
    • 1970-01-01
    • 2021-03-31
    相关资源
    最近更新 更多