[英]Weird C# GC behavior for Weakreferences and the null-conditional operator
在為與WeakReferences
一起使用的C#代碼創建單元測試時,我遇到了一些怪異的GC行為-怪異的,因為我無法對此做出解釋。
該問題源於使用?.
從GC是要從我的弱引用中獲取的對象上獲取的空條件運算符。
這是復制它的最少代碼:
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),但仍然無法識別任何錯誤。
這與null條件運算符無關。 通過將其替換為普通成員訪問權限,您可以輕松地看到它:
Console.WriteLine(t == null); //outputs false
Console.WriteLine(t.Name == null); //outputs false
對新XYZClass
對象的原始引用在調試版本中永遠不會超出范圍(並在調試器下運行)。 在LINQPad中關閉優化功能,您還將看到t
不為null。 但是請注意,所有這些都是實現細節-根據系統的具體情況,您可能會得到兩種結果(例如,我得到的是32位Debug構建,而不是64位Debug構建)。
關於.NET中托管對象生存期的唯一保證是,終結器外部的強引用將阻止對象的收集。 忘記所有確定性的內存管理-它只是不存在。 完全沒有垃圾收集器的.NET實現將是完全有效的。
因此,讓我們特別看看在我的機器上生成的代碼。 在64位版本中, t.Name == null
和t?.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
您會發現,空條件版本使用的是用於保存對XYZClass
的臨時引用的同一寄存器。 這就是差異的根源所在-運行時不能排除edx
訪問是使用臨時引用,因此它可以安全地運行並保持對象的根目錄,從而防止收集該對象。
64位版本(並且在未連接調試器的情況下運行)沒有什么區別,因為它重用了不同的寄存器-在我的特定機器上,64位版本重用了rcx
(它包含對WeakReference
,而不是XYZClass
) ,並且非調試器的32位版本重復使用eax
(其中包含對"bleh"
的引用)。 由於edx
(和rdx
)從未在該方法中使用,因此臨時引用不再植根,可以自由收集。
為什么調試器版本特別使用edx
? 最有可能的是,它試圖有所幫助。 在空條件運算符的中間,您希望同時看到t
和t?.Name
,以便更好地訪問它們(您可以在Locals中將其作為“ XYZClass.Name.get返回的“ bleh”字符串”查看)。 。
同樣,請注意,這完全是特定於實現的。 該合同只規定當一個對象不能被回收-它並沒有說何時會被回收。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.