为什么在卸载事件触发后 AppDomain 仍然引用动态加载的程序集?

问题描述

这是一个 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 未能更新其簿记?你会怎么说?

解决方法

在灵感的时刻,我尝试了一个发布版本,问题消失了。但为什么?评论者 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#?

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...