Weakreferences 和 null 条件运算符的奇怪 C# GC 行为

Weird C# GC behavior for Weakreferences and the null-conditional operator

在为使用 WeakReferences 的 C# 代码创建单元测试时,我 运行 遇到了一些奇怪的 GC 行为 - 很奇怪,因为我无法对此做出解释.

问题源于在 GC 打算收集它之后从我的弱引用获取的对象上使用 ?. null 条件运算符

下面是复制它的最少代码:

    public class XYZClass
    {
        public string Name { get; set; }
    }

    public class Tests
    {
        public void NormalBehavior()
        {
            var @ref = new WeakReference<XYZClass>(new XYZClass { Name = "bleh" });

            GC.Collect();
            GC.WaitForPendingFinalizers();

            XYZClass t;
            @ref.TryGetTarget(out t);

            Console.WriteLine(t == null); //outputs true
        }

        public void WeirdBehavior()
        {
            var @ref = new WeakReference<XYZClass>(new XYZClass { Name = "bleh" });

            GC.Collect();
            GC.WaitForPendingFinalizers();

            XYZClass t;
            @ref.TryGetTarget(out t);

            Console.WriteLine(t == null); //outputs false
            Console.WriteLine(t?.Name == null); //outputs false
        }
    }

当此代码 运行 使用 linqpad 时,不会出现此行为。我还检查了编译的 IL 代码(使用 linqpad),但仍然无法识别任何错误。

这与空条件运算符无关。您可以通过将其替换为普通成员访问权限来轻松看到它:

Console.WriteLine(t == null); //outputs false
Console.WriteLine(t.Name == null); //outputs false

对新 XYZClass 对象的原始引用在调试版本中永远不会 "out of scope"(在调试器下 运行)。在 LINQPad 中关闭优化,您还会看到 t 不为空。但请注意,所有这些都是一个实现细节 - 根据您系统的具体情况,您可以获得任一结果(例如,我得到了您在 32 位调试版本上获得的结果,但不是 64 位调试版本)。

关于 .NET 中托管对象生命周期的唯一保证是终结器外部的强引用将阻止收集对象。忘掉所有确定性的内存管理——它就是不存在。完全没有垃圾收集器的 .NET 实现是完全有效的。

因此,让我们特别看看在我的机器上生成的代码。在 64 位版本中,t.Name == nullt?.Name == null 具有完全相同的结果(当然 t.Name == null 会导致 NullReferenceException 而不是返回 true)。 32 位版本怎么样?

t.Name == null 部分大大缩短了:

00533111  mov         ecx,dword ptr [ebp-44h]   ; t
00533114  cmp         dword ptr [ecx],ecx  ; null check
00533116  call        00530D28  ; t.get_Name
0053311B  mov         dword ptr [ebp-54h],eax  ; Name string
0053311E  cmp         dword ptr [ebp-54h],0  ; is null?
00533122  sete        cl  
00533125  movzx       ecx,cl  
00533128  call        708B09F4  

你可以看到我们使用了两个寄存器(ecx 和 eax)和两个栈槽(-44h 和 -54h)。 t?.Name == null 那个怎么样?

001F3111  cmp         dword ptr [ebp-44h],0   ; is t null?
001F3115  jne         001F311F  
001F3117  nop  
001F3118  xor         edx,edx  
001F311A  mov         dword ptr [ebp-54h],edx  ; result is false
001F311D  jmp         001F312A  
001F311F  mov         ecx,dword ptr [ebp-44h]  ; t
001F3122  call        001F0D28                 ; t.get_Name
001F3127  mov         dword ptr [ebp-54h],eax  
001F312A  cmp         dword ptr [ebp-54h],0    ; is name null?
001F312E  sete        cl  
001F3131  movzx       ecx,cl  
001F3134  call        708B09F4  
001F3139  nop  

我们仍在使用相同的两个堆栈槽,但需要另一个寄存器 - edx。这会是我们要找的吗?完全正确!如果我们查看对象最初是如何创建的:

001F30A0  mov         ecx,2C0814h  
001F30A5  call        001330F4  ; new XYZClass
001F30AA  mov         dword ptr [ebp-48h],eax  ; tmp
001F30AD  mov         ecx,dword ptr [ebp-48h]  
001F30B0  call        001F0D38  ; tmp.XYZClass()
001F30B5  mov         edx,dword ptr ds:[36B230Ch]  ; "bleh"
001F30BB  mov         ecx,dword ptr [ebp-48h]  
001F30BE  cmp         dword ptr [ecx],ecx  
001F30C0  call        001F0D30  ; tmp.set_Name("bleh")
001F30C5  nop  
001F30C6  mov         ecx,2C0858h  
001F30CB  call        710F9ECF  ; new WeakReference
001F30D0  mov         dword ptr [ebp-4Ch],eax  
001F30D3  mov         ecx,dword ptr [ebp-4Ch]  
001F30D6  mov         edx,dword ptr [ebp-48h]  ; EDX references tmp!
001F30D9  call        709090B0  
001F30DE  mov         eax,dword ptr [ebp-4Ch]  
001F30E1  mov         dword ptr [ebp-40h],eax  

您可以看到,null 条件版本使用了用于保存对 XYZClass 的临时引用的同一寄存器。这就是差异的来源 - 运行时不能排除 edx 访问是对临时引用的使用,因此它可以安全地发挥作用并使对象保持根目录,从而防止它被收集。

64 位版本(和未附加调试器的 运行)看不出区别,因为它重用了不同的寄存器 - 在我的特定机器上,64 位版本重用 rcx(其中包含对 WeakReference 的引用,而不是 XYZClass),非调试器 32 位版本重用 eax(其中包含对 "bleh" 的引用)。由于 edx(和 rdx)从未在该方法中使用,因此临时引用不再是根目录,可以自由收集。

为什么调试器版本特别使用edx?最有可能的是,它试图提供帮助。在 null 条件运算符的中间,您想查看 tt?.Name 的值,以便更好地访问它们(您可以在 Locals 中将其视为 "XYZClass.Name.get returned "bleh" 字符串").

再次注意,这完全是特定于实现的。合同仅指定何时 不得 回收对象 - 它没有说明何时 回收。