我目前正在调试我公司的 CLR 探查器(超过 4.7.3282.0,.NET Framework 4.7.2 ASP.NET),并看到 CLR 卸载泛型类,但没有调用 ClassUnloadStarted 回调的情况。
简而言之,我们的分析器根据 ClassID 跟踪加载的类,遵循 ClassLoadStarted、ClassLoadFinish和ClassUnloadStarted回调。在某些时候,类被卸载(连同其相关模块),但不会为相关的ClassID 调用 ClassUnloadStarted回调。因此,我们只剩下一个停滞的 ClassID,认为该类仍在加载。稍后,当我们尝试查询该 ClassID 时,CLR 不出所料地崩溃(因为它现在指向垃圾内存)。
我的问题,考虑到下面的详细情况:
- 为什么我的(泛型)类不调用 ClassUnloadStart?
- 这是 CLR 的预期边缘情况行为,还是可能是 CLR/分析 API 错误?
我找不到任何关于这种行为的文档或推理,特别是没有调用 ClassUnloadStart。我在 CoreCLR 代码中也找不到任何提示。提前感谢任何帮助!
详细场景:
这是有问题的类(IComparable(T)
与T=ClassFromModuleFoo
):
System/IComparable`1<ClassFromModuleFoo>
当应用程序运行时,问题会在卸载某些模块后出现。
下面是基于添加的调试打印的确切加载/卸载回调流:
- 加载了 mscorlib 的类
System/IComparable'1(ClassFromModuleFoo)
。 - 紧接着,模块 Foo 的类
ClassFromModuleFoo
被加载到程序集 #1 中。 - 模块 Foo 完成以加载到程序集 #1 中。
- 然后,模块Foo再次加载到不同的程序集#2中。
- 再次加载
IComparable
和ClassFromModuleFoo
,这次在程序集 #2 中加载。现在每个类有两个实例:一个在程序集 #1 中加载的 Foo 中,另一个在程序集 #2 中加载的 Foo 中。 - 模块 Foo 开始从程序集 #1 卸载。
ClassUnloadStarted
回调是为程序集 #1 中的ClassFromModuleFoo
调用的。- 模块 Foo 已完成从组件 #1 卸载。 以后不会为
- 程序集 #1 的
System/IComparable'1(ClassFromModuleFoo)
调用ClassUnloadStarted
(即使其模块已卸载并且其 ClassID 指向现在已破坏的内存)。
一些附加信息:
- 此问题还会在最新的 .NET 框架版本 4.8 预览版中重现。
- 我通过向探查器事件掩码添加
COR_PRF_DISABLE_ALL_NGEN_IMAGES
来禁用本机图像,认为这可能会影响 ClassLoad* 回调,但这没有任何区别。我验证了mscorlib.dll
确实加载了而不是其本机映像。
编辑:
感谢我非常聪明的同事,我能够通过一个小的示例项目重现该问题,该项目通过加载和卸载 AppDomain 来模拟这种情况。这是:
https://github.com/shaharv/dotnet/tree/master/testers/module-load-unload
此类在测试中发生崩溃,该类已卸载,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)
{
}
}
}
}
在某些时候,探查器在尝试查询该类的 ClassID 时崩溃 - 认为它仍然有效,因为没有为其调用卸载回调。
附带说明一下,我尝试将此示例移植到 .NET Core 以进行进一步调查,但无法弄清楚如何,因为 .NET Core 不支持辅助 AppDomain(而且我不太确定它通常是否支持按需程序集卸载)。
在.Net Core中使之成为可能(在3.0之前不支持卸载)之后,我们设法复制了它(感谢valiano!它被 coreclr 团队 (https://github.com/dotnet/coreclr/issues/26126) 确认为一个错误。
从达夫梅森的解释中:
涉及三种不同的类型,每个回调仅 给你两个(但不同的一组两个)。
Plugin.MyGenList1:未绑定的泛型类型 Plugin.MyGenList1 :绑定到规范类型的泛型类型(使用 对于普通引用)插件.MyGenList1:通用 类型绑定到系统字符串。对于 ClassLoadStarted,我们有逻辑 明确排除未绑定的泛型类型(即 Plugin.MyGenList1) 从显示到分析器在 类加载器::通知
这意味着您的 ClassLoadStarted 只为您提供 规范实例和字符串实例。这似乎是正确的做法, 因为作为探查器,您只关心绑定泛型类型和 对于未绑定的人来说,没有什么有趣的。
问题是我们对 类卸载已启动。该回调发生在 EEClass::D estruct 中,并且 仅在非泛型类型、未绑定泛型类型、 和规范泛型类型。非规范泛型类型(即 Plugin.MyGenList1 ) 被跳过。