ICorProfilerCallback::ClassUnloadStarted 未调用通用 class,即使 class 已卸载

ICorProfilerCallback::ClassUnloadStarted not called for a generic class, even though the class was unloaded

我目前正在调试我公司的 CLR 探查器(在 ASP.NET 4.7.3282.0,.NET Framework 4.7.2 上),并看到 CLR 卸载通用 class 的场景,但是 ClassUnloadStarted 回调没有被调用。

简而言之,我们的分析器根据 ClassID 跟踪已加载的 classes,遵循 ClassLoadStartedClassLoadFinishedClassUnloadStarted 回调。在某些时候,class 被卸载(连同其相关模块),但不会为相关的 ClassID 调用 ClassUnloadStarted 回调。因此,我们剩下一个停顿的 ClassID,认为 class 仍然被加载。稍后,当我们尝试查询该 ClassID 时,CLR 毫不奇怪地崩溃了(因为它现在指向垃圾内存)。

我的问题,考虑到以下详细情况:

我找不到关于此行为的任何文档或推理,特别是 ClassUnloadStarted 未被调用。我在 CoreCLR 代码中也找不到任何提示。在此先感谢您的帮助!

详细场景:

这是有问题的 class(IComparable(T)T=ClassFromModuleFoo):

System/IComparable`1<ClassFromModuleFoo>

当应用程序运行时,问题会在卸载某些模块后出现。
这是确切的 load/unload 回调流程,基于添加的调试打印:

  1. 已加载 mscorlib 的 class System/IComparable'1(ClassFromModuleFoo)
  2. 紧接着,模块 Foo 的 class ClassFromModuleFoo 被加载到程序集 #1 中。
  3. 模块 Foo 完成加载到程序集 #1。
  4. 然后,模块 Foo 再次加载到另一个程序集 #2。
  5. 再次加载 IComparableClassFromModuleFoo,这次是在程序集 #2 中。现在每个 class 有两个实例:一个在程序集 #1 中加载的 Foo 中,一个在程序集 #2 中加载的 Foo 中。
  6. 模块 Foo 开始从程序集 #1 卸载。
  7. ClassUnloadStarted 在程序集 #1 中为 ClassFromModuleFoo 调用回调。
  8. 模块 Foo 已完成从程序集 #1 中卸载。
  9. ClassUnloadStartednot 稍后随时调用程序集 #1 的 System/IComparable'1(ClassFromModuleFoo) (即使它的模块已卸载并且它的 ClassID 指向现在已崩溃的内存) .

一些附加信息:

编辑:

感谢我非常聪明的同事,我能够通过一个小示例项目重现该问题,该项目通过加载和卸载 AppDomains 来模拟这种情况。在这里:
https://github.com/shaharv/dotnet/tree/master/testers/module-load-unload

测试中的这个 class 发生崩溃,它已卸载,并且 CLR 没有为其调用卸载回调:

Loop/MyGenList`1<System/String>

下面是相关代码,加载卸载了几次:

namespace Loop
{
    public class MyGenList<T>
    {
        public List<T> _tList;

        public MyGenList(List<T> tList)
        {
            _tList = tList;
        }
    }

    class MyGenericTest
    {
        public void TestFunc()
        {
            MyGenList<String> genList = new MyGenList<String>(new List<string> { "A", "B", "C" });

            try
            {
                throw new Exception();
            }
            catch (Exception)
            {

            }
        }
    }
}

在某些时候,探查器在尝试查询那个 class 的 ClassID 时崩溃了 - 认为它仍然有效,因为没有为它调用卸载回调。

附带说明一下,我尝试将此示例移植到 .NET Core 以进行进一步调查,但无法弄清楚如何,因为 .NET Core 不支持辅助 AppDomain(我不太确定)一般支持按需程序集卸载。

在 .Net Core 中使其成为可能后(3.0 之前不支持卸载),我们设法复制了它(感谢 valiano!)。已被 coreclr 团队 (https://github.com/dotnet/coreclr/issues/26126) 确认为错误。

来自davmason的解释:

There are three separate types involved and each callback is only giving you two (but a different set of two).

Plugin.MyGenList1: the unbound generic type Plugin.MyGenList1 : the generic type bound to thecanonical type (used for normal references) Plugin.MyGenList1 : the generic type bound to System.String. For ClassLoadStarted we have logic that that specifically excludes unbound generic types (i.e. Plugin.MyGenList1) from being shown to the profiler in ClassLoader::Notify

This means you ClassLoadStarted only gives you callbacks for the canonical and string instances. This seems the right thing to do here, since as a profiler you would only care about bound generic types and there's nothing of interest for unbound ones.

The issue is that we do a different set of filtering for ClassUnloadStarted. That callback occurs inside EEClass::Destruct, and Destruct is only called on non-generic types, unbound generic types, and canonical generic types. Non-canonical generic types ( i.e. Plugin.MyGenList1 ) are skipped.