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,遵循 ClassLoadStarted、ClassLoadFinished和 ClassUnloadStarted 回调。在某些时候,class 被卸载(连同其相关模块),但不会为相关的 ClassID 调用 ClassUnloadStarted 回调。因此,我们剩下一个停顿的 ClassID,认为 class 仍然被加载。稍后,当我们尝试查询该 ClassID 时,CLR 毫不奇怪地崩溃了(因为它现在指向垃圾内存)。
我的问题,考虑到以下详细情况:
- 为什么我的(通用)没有调用 ClassUnloadStarted class?
- 这是预期的 CLR 边缘情况行为,还是可能是 CLR/Profiling API 错误?
我找不到关于此行为的任何文档或推理,特别是 ClassUnloadStarted 未被调用。我在 CoreCLR 代码中也找不到任何提示。在此先感谢您的帮助!
详细场景:
这是有问题的 class(IComparable(T)
和 T=ClassFromModuleFoo
):
System/IComparable`1<ClassFromModuleFoo>
当应用程序运行时,问题会在卸载某些模块后出现。
这是确切的 load/unload 回调流程,基于添加的调试打印:
- 已加载 mscorlib 的 class
System/IComparable'1(ClassFromModuleFoo)
。
- 紧接着,模块 Foo 的 class
ClassFromModuleFoo
被加载到程序集 #1 中。
- 模块 Foo 完成加载到程序集 #1。
- 然后,模块 Foo 再次加载到另一个程序集 #2。
- 再次加载
IComparable
和 ClassFromModuleFoo
,这次是在程序集 #2 中。现在每个 class 有两个实例:一个在程序集 #1 中加载的 Foo 中,一个在程序集 #2 中加载的 Foo 中。
- 模块 Foo 开始从程序集 #1 卸载。
ClassUnloadStarted
在程序集 #1 中为 ClassFromModuleFoo
调用回调。
- 模块 Foo 已完成从程序集 #1 中卸载。
ClassUnloadStarted
是 not 稍后随时调用程序集 #1 的 System/IComparable'1(ClassFromModuleFoo)
(即使它的模块已卸载并且它的 ClassID 指向现在已崩溃的内存) .
一些附加信息:
- 该问题还会在最新的 .NET Framework 版本 4.8 预览版中重现。
- 我通过将
COR_PRF_DISABLE_ALL_NGEN_IMAGES
添加到探查器事件掩码来禁用本机图像,认为它可能会影响 ClassLoad* 回调,但它没有任何区别。我确认确实加载了 mscorlib.dll
而不是其原始图像。
编辑:
感谢我非常聪明的同事,我能够通过一个小示例项目重现该问题,该项目通过加载和卸载 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.
我目前正在调试我公司的 CLR 探查器(在 ASP.NET 4.7.3282.0,.NET Framework 4.7.2 上),并看到 CLR 卸载通用 class 的场景,但是 ClassUnloadStarted 回调没有被调用。
简而言之,我们的分析器根据 ClassID 跟踪已加载的 classes,遵循 ClassLoadStarted、ClassLoadFinished和 ClassUnloadStarted 回调。在某些时候,class 被卸载(连同其相关模块),但不会为相关的 ClassID 调用 ClassUnloadStarted 回调。因此,我们剩下一个停顿的 ClassID,认为 class 仍然被加载。稍后,当我们尝试查询该 ClassID 时,CLR 毫不奇怪地崩溃了(因为它现在指向垃圾内存)。
我的问题,考虑到以下详细情况:
- 为什么我的(通用)没有调用 ClassUnloadStarted class?
- 这是预期的 CLR 边缘情况行为,还是可能是 CLR/Profiling API 错误?
我找不到关于此行为的任何文档或推理,特别是 ClassUnloadStarted 未被调用。我在 CoreCLR 代码中也找不到任何提示。在此先感谢您的帮助!
详细场景:
这是有问题的 class(IComparable(T)
和 T=ClassFromModuleFoo
):
System/IComparable`1<ClassFromModuleFoo>
当应用程序运行时,问题会在卸载某些模块后出现。
这是确切的 load/unload 回调流程,基于添加的调试打印:
- 已加载 mscorlib 的 class
System/IComparable'1(ClassFromModuleFoo)
。 - 紧接着,模块 Foo 的 class
ClassFromModuleFoo
被加载到程序集 #1 中。 - 模块 Foo 完成加载到程序集 #1。
- 然后,模块 Foo 再次加载到另一个程序集 #2。
- 再次加载
IComparable
和ClassFromModuleFoo
,这次是在程序集 #2 中。现在每个 class 有两个实例:一个在程序集 #1 中加载的 Foo 中,一个在程序集 #2 中加载的 Foo 中。 - 模块 Foo 开始从程序集 #1 卸载。
ClassUnloadStarted
在程序集 #1 中为ClassFromModuleFoo
调用回调。- 模块 Foo 已完成从程序集 #1 中卸载。
ClassUnloadStarted
是 not 稍后随时调用程序集 #1 的System/IComparable'1(ClassFromModuleFoo)
(即使它的模块已卸载并且它的 ClassID 指向现在已崩溃的内存) .
一些附加信息:
- 该问题还会在最新的 .NET Framework 版本 4.8 预览版中重现。
- 我通过将
COR_PRF_DISABLE_ALL_NGEN_IMAGES
添加到探查器事件掩码来禁用本机图像,认为它可能会影响 ClassLoad* 回调,但它没有任何区别。我确认确实加载了mscorlib.dll
而不是其原始图像。
编辑:
感谢我非常聪明的同事,我能够通过一个小示例项目重现该问题,该项目通过加载和卸载 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.