[C#] 淺談對象析構器的調用時機與作用域的問題

        今天和群友偶然聊到了一個關於靜態字段導致的內存泄露(群友「海星」的文章:https://starfishpeter.cn/archives/8497)。聊到這個內存上的問題,就很容易導向到對CLR的GC機制的討論。

        而我們都知道,CLR在回收對象前,如果對象通過析構函數(~className())來重寫了Finalize方法,第一次GC時就會先將它從「終結列表」中移除,添加到freachable隊列中並調用它們的Finalize方法。這部分的內容我也做了相應的筆記(CLR「托管堆和垃圾回收」- 終結的內部工作原理)。

       然而今天在群友做析構函數的測試時(看內存有沒有被回收)時,卻發現無論如何,都沒有辦法回收資源。無論手動調用多少次GC.Collect,甚至這個對象模型中沒有一個靜態成員,這個對象也沒有被回收(當然,這個對象已經是置為null的了),下圖為群友的案例:

        最後經過了多次向Google大神的請教,終於找到了比較具體的兩個條目:

    1. Microsoft的C#官方文檔——Object.Finalize方法:這個文檔裡提到了Finalize方法的限制以及可能不會執行的原因
    2. MSDN上manuelalonge的一個討論條目——「Why is the destructor not making his job?」

        這兩個條目,尤其是第二個,提到了一個很重要的點:

“The destructor will not be called until p1 and p2 go out of scope. Because they are declared in the Main method, this will not happen until the program has ended, so you will not have a chance to see “Destructor called”.”

        簡單來說,就是作用域的問題。如果對象的作用域是在像Main函數這樣與程序運行同生共死的區域中,這個對象的內存(那怕變量已經置空了)會在程序結束運行後才被回收,而析構器也會在程序結束的時候才被調用。

        換而言之,如果對象的作用域是在單獨的一個函數(如:static void DoStuff())中,再在Main裡調用這個函數DoStuff(),然後調用GC.Collect(),那一切將能正常運行。因為對象的作用域是在DoStuff函數內,DoStuff函數執行完畢後,函數裡聲明的對象內存也會隨之被回收,析構函數也能正常被調用。如以下測試用例: