即使类已卸载,也不会为通用类调用ICorProfilerCallback :: ClassUnloadStarted

val*_*ano 7 .net generics clr profiling clr-profiling-api

我目前正在调试公司的CLR探查器(通过ASP.NET 4.7.3282.0,.NET Framework 4.7.2),并看到CLR卸载通用类但未调用ClassUnloadStarted回调的情况。

简而言之,我们的探查器根据ClassLoadStartedClassLoadFinishedClassUnloadStarted回调跟踪基于ClassID的加载。在某些时候,该类(及其相关模块)将被卸载,但相关类ID不会调用ClassUnloadStarted回调。因此,我们剩下一个停滞的ClassID,以为该类仍在加载。稍后,当我们尝试查询该ClassID时,CLR意外崩溃(因为它现在指向垃圾内存)。

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

  • 为什么不为我的(通用)类调用ClassUnloadStarted?
  • 这是CLR的预期的极端情况行为,还是CLR /分析API错误?

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

详细方案:

这是所讨论的类(IComparable(T)带有T=ClassFromModuleFoo):

System/IComparable`1<ClassFromModuleFoo>
Run Code Online (Sandbox Code Playgroud)

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

  1. System/IComparable'1(ClassFromModuleFoo)mscorlib 的类已加载。
  2. 之后,立即ClassFromModuleFoo将模块Foo 的class 加载到程序集#1中。
  3. Foo模块完成加载到部件1中。
  4. 然后,模块Foo再次装入另一个组件#2。
  5. IComparableClassFromModuleFoo再次加载,这一次是在组装#2。现在,每个类有两个实例:一个在装配#1中加载的Foo中,一个在装配#2中加载的Foo中。
  6. 模块Foo开始从部件#1卸载。
  7. ClassUnloadStartedClassFromModuleFoo在程序集1中需要回调。
  8. Foo模块完成了从组件#1卸载的工作。
  9. ClassUnloadStarted以后任何时候都不会调用System/IComparable'1(ClassFromModuleFoo)程序集1(即使已卸载其模块且其ClassID指向现在已损坏的内存)。

一些其他信息:

  • 该问题还会在最新的.NET Framework版本4.8 Preview中重现。
  • 我已通过添加COR_PRF_DISABLE_ALL_NGEN_IMAGES到事件探查器事件掩码来禁用本机映像,认为它可能会影响ClassLoad *回调,但并没有任何区别。我验证mscorlib.dll了确实在加载而不是其本机映像。

编辑:

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

崩溃发生在测试中的此类上,该类已被卸载,并且CLR没有针对该类调用卸载回调:

Loop/MyGenList`1<System/String>
Run Code Online (Sandbox Code Playgroud)

这是相关的代码,它被加载和卸载了几次:

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)
            {

            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在某个时候,探查器在尝试查询该类的ClassID时崩溃-认为它仍然有效,因为未为其调用回调。

附带说明一下,我尝试将此示例移植到.NET Core进行进一步的研究,但无法弄清楚怎么做,因为.NET Core不支持辅助AppDomain(而且我不太确定它是否支持按需组装)卸载)。

Ego*_*ozy 1

在 .Net Core 中实现这一功能后(3.0 之前不支持卸载),我们成功地复制了它(感谢 valiano!)。coreclr 团队确认这是一个错误(https://github.com/dotnet/coreclr/issues/26126)。

根据达马森的解释:

涉及三种不同的类型,每个回调只给您两个(但两个不同的集合)。

Plugin.MyGenList1:未绑定的泛型类型 Plugin.MyGenList1:绑定到规范类型的泛型类型(用于普通引用) Plugin.MyGenList1:绑定到 System.String 的泛型类型。对于 ClassLoadStarted,我们有专门排除未绑定泛型类型(即 Plugin.MyGenList1)向 ClassLoader::Notify 中的探查器显示的逻辑

这意味着 ClassLoadStarted 只为您提供规范实例和字符串实例的回调。这似乎是正确的做法,因为作为探查器,您只关心绑定的泛型类型,而对未绑定的类型没有任何兴趣。

问题是我们对 ClassUnloadStarted 进行了一组不同的过滤。该回调发生在 EEClass::Destruct 内部,并且 Destruct 仅在非泛型类型、未绑定泛型类型和规范泛型类型上调用。跳过非规范泛型类型(即 Plugin.MyGenList1 )。