我目前正在调试我公司的 CLR 分析器(通过 ASP.NET 4.7.3282.0、.NET 框架 4.7.2),并看到 CLR 卸载泛型类但未调用ClassUnloadStarted回调的场景。
简而言之,我们的分析器根据 ClassID 跟踪加载的类,遵循ClassLoadStarted、ClassLoadFinished和ClassUnloadStarted回调。在某些时候,类会被卸载(连同其相关模块),但不会为相关 ClassID 调用ClassUnloadStarted回调。因此,我们留下了一个停顿的 ClassID,认为该类仍在加载。稍后,当我们尝试查询该 ClassID 时,CLR 不出所料地崩溃了(因为它现在指向垃圾内存)。
我的问题,考虑到下面的详细情况:
- 为什么我的(通用)类没有调用 ClassUnloadStarted?
- 这是 CLR 的预期边缘情况行为,还是可能是 CLR/Profiling API 错误?
我找不到任何关于此行为的文档或推理,特别是ClassUnloadStarted没有被调用。我在 CoreCLR 代码中也找不到任何提示。提前感谢您的帮助!
详细场景:
这是有问题的课程(IComparable(T)
with T=ClassFromModuleFoo
):
System/IComparable`1<ClassFromModuleFoo>
在应用程序运行时,问题在某些模块被卸载后出现。
这是基于添加的调试打印的确切加载/卸载回调流程:
- mscorlib的类
System/IComparable'1(ClassFromModuleFoo)
已加载。 - 紧接着,
ClassFromModuleFoo
模块 Foo 的类被加载到程序集#1 中。 - 模块 Foo 完成加载到程序集 #1。
- 然后,模块 Foo 再次加载到不同的程序集#2 中。
IComparable
和再次加载,这次ClassFromModuleFoo
是在程序集 #2 中。现在每个类有两个实例:一个在 Foo 中加载到程序集 #1 中,另一个在 Foo 中加载在程序集 #2 中。- 模块 Foo 开始从程序集 #1 中卸载。
ClassUnloadStarted
ClassFromModuleFoo
在程序集 #1 中调用回调。- 模块 Foo 已完成从程序集 #1 中卸载。
ClassUnloadStarted
以后的任何时候都不会调用System/IComparable'1(ClassFromModuleFoo)
程序集#1(即使它的模块已卸载并且它的 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(而且我不太确定它是否支持按需组装一般卸货)。