首页 > 解决方案 > 为什么在 unload 事件触发后 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 未能更新其簿记?你会怎么说?

标签: c#.net-coreassembly-loading

解决方案


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

  }
}

我们在 Debug 构建中有完美的行为。

请注意,必要显式执行垃圾收集。为什么 GC最终不会自行发生?在一个更大的应用程序中,它会做更多的事情,但是在这个测试的封闭范围内没有更多的对象被创建,所以没有达到内存压力等的触发阈值。

通常是什么触发它?答案就在这里:什么时候在 C# 中触发垃圾收集?


推荐阅读