前些天在論壇裡看到了一篇帖子垃圾收集問題——是不是bug其問題如下:
static tc gto;
public class tc
{
public int a=99;
~tc()
{
a=-1; //set breakpoint 1
gto=this;
}
}
private void button1_Click(object sender,EventArgs e)
{
tc to=new tc();
GC.Collect();
GC.WaitForPendingFinalizers();
return; // set breakpoint 2
}
two problems:
click button once,it seem the ~tc() not execute at all.and it will excute at the second time clicking.why?
the second clicking,breakpoint 2, "gto" and "to", gto.a=-1,while to.a=99,why?
問題很好解答第一次強制回收的時候並不能回收局部變量to,只有下次點擊的時候才會回收上次產生的tc對象所以~tc()沒有立即執行。而第二個問題是因為在析構函數中將對象的a置為-1並將引用賦值給了一個全局對象所以gto.a=-1,而新new出的來的tc對象a自然為99.我本來對第一個問題的解答信心滿滿,但當我正要回答問題時我看到了gomoku 的回復,他說到:
1,在調試時,Visual Studio提供了顯示即時值的功能,比如讓你的breakpoint 2的地方,能夠檢查tc的值。
要檢查tc的值,這就要求tc不能被垃圾回收,為此Visual Studio調試器插入了特殊代碼以保證tc在離開button1_Click()以前不會被回收。
2,Release版就不同了。你把例子用Release編譯運行,就會發現,同Debug相反,~tc()在第一次GC.Collect時就被調用了。
我立刻動手進行試驗,發現release下果然如gomoku所說我不免對自己的理解有所懷疑。因為我深信這肯定和vs的斷點無關,而且msdn,.net書籍,國內外技術文章在講解垃圾回收原理時都是這麼解釋的”一開始gc把托管堆上的對象都視為可回收的,然後從應用程序的一組根出發尋找可達對象,不可達的對象則被回收。由JIT編譯器和CLR運行時維護根指針列表,主要包括全局變量、靜態變量、局部變量和寄存器指針等“所以我認為即使在release下也因該和debug下表現相同,除非release下有優化操作根本就不會保存這個局部變量。所以我馬上看debug和release兩個版本的IL
debug:
IL_0000: nop
IL_0001: newobj tc::.ctor
IL_0006: stloc.0
IL_0007: call System.GC::Collect
IL_000c: nop
IL_000d: call System.GC::WaitForPendingFinalizers
IL_0012: nop
IL_0013: br.s IL_0015
IL_0015: ret
release:
ilAddr = 01394590
IL_0000: newobj tc::.ctor
IL_0005: pop
IL_0006: call System.GC::Collect
IL_000b: call System.GC::WaitForPendingFinalizers
IL_0010: ret
我們看到debug和release有明顯的區別,debug下有IL_0006: stloc.0 指令而release下載new完tc後直接就pop了。ok有了il墊底只要再看jitted後的機器匯編就能確定msdn上說的確實沒錯,不過 …… 郁悶的事情發生了。從vs中看debug和release的反匯編基本沒有區別棧上都保存了局部變量to的值。想了半天才換然大悟gomoku的回復中說了vs為了調試方便給反匯編中加了一些代碼所以我看到的debug和release下是一樣的。
好了後面的事情就簡單了,就是一個搜集證據的過程。不過更郁悶的事情發生了,我用了2天時間才弄白windbg怎麼獲得release下方法的jitted後匯編(真理真是很難被驗證的,為了證明一個真理你需要多付出幾倍甚至與幾十倍的代價來揭穿謊言和謬誤,當然我不是說gomoku說的是謬誤只是並沒有說到問題的關鍵點上),這裡我特別要感謝lbq的文章和cici對我的指導。
debug下主要反匯編:
mov dword ptr [ebp-10h],eax //new出來的對象地址
mov ecx,dword ptr [ebp-10h]
call dword ptr ds:[806DE8h] (Test_WinForm.GC_Test+tc..ctor(), mdToken: 0600004f)
mov eax,dword ptr [ebp-10h]
mov dword ptr [ebp-0Ch],eax //在棧上保存對象地址
call mscorlib_ni+0x6b7bcc (6c6a7bcc) (System.GC.Collect(), mdToken: 06000ab8) //強制垃圾回收
nop
call mscorwks!GCInterface::RunFinalizers (6cbe3491)
release下反匯編:
mov ebp,esp
mov ecx,7D6714h (MT: Test_WinForm.GC_Test+tc)
call mscorwks!JIT_NewFast (6cafc5d4) //new tc()
mov dword ptr [eax+4],63h //將99復制給對象地址+4(這裡就是a的位置),相當於構造函數
xor edx,edx
lea ecx,[edx-1]
call mscorwks!GCInterface::CollectGeneration (6cbbd082)
call mscorwks!GCInterface::RunFinalizers (6cbe3491)
pop ebp
ret 4
在這裡要說明一點,雖然在強制調用gc前EAX中保存了tc的對象地址,但因為EAX這個寄存器只是方法調用後返回值的所以不能確保在這個寄存器的值不會被替換掉(通過跟蹤GCInterface::CollectGeneration發現該函數一開始就把eax中的值替換掉了)。既然明白了垃圾回收時確定可達對象的原理我們就可以做一個小小的實驗了,在強制垃圾後面加入這條語句:
private void button1_Click(object sender,EventArgs e)
{
tc to=new tc();
GC.Collect();
GC.WaitForPendingFinalizers();
string s = tc.ToString();
return;
}
這時在release下運行其運行結果就和debug下一樣了必須要點擊第二次才會執行析構函數,讓我們再看看此時的反匯編:
mov ebp,esp
push esi
mov ecx,7D6714h (MT: Test_WinForm.GC_Test+tc)
call mscorwks!JIT_NewFast (6cafc5d4)
mov esi,eax //將對象地址賦值給esi
mov dword ptr [esi+4],63h
xor edx,edx
lea ecx,[edx-1]
call mscorwks!GCInterface::CollectGeneration (6cbbd082)
call mscorwks!GCInterface::RunFinalizers (6cbe3491)
mov ecx,esi //將保存在esi中的值返回個ecx
mov eax,dword ptr [ecx]
call dword ptr [eax+28h] //調用ToString()
pop esi
pop ebp
ret 4
我跟蹤了整個gc函數發現雖然在函數體內替換了esi的值但是函數結束後esi的值被恢復了而eax的值是不會被恢復的,現在還不明白原理。實際上本文沒有什麼特別的東西只是證明了一下gc理論和jit優化的能力。