2013-04-21 37 views
7

我已經穿過其中的垃圾回收似乎是相同的代碼之間的不同表現運行寫成一個單元測試VS寫在一個控制檯應用程序的Main方法的情況下絆倒。我想知道這種差異背後的原因。不同的垃圾收集行爲

在這種情況下,我和同事在分配垃圾收集事件處理程序的效果上存在分歧。我認爲比簡單地給他發送一個鏈接到highly rated SO answer的示例會更好。因此我寫了一個簡單的演示作爲單元測試。

我的單元測試顯示事情的工作,因爲我說他們應該。但是,我的同事編寫了一個控制檯應用程序,顯示事情正在按照他的方式工作,這意味着GC未按照我對Main方法中的本地對象的預期發生。只需將我的測試代碼移動到控制檯應用程序項目的Main方法中,我就能夠重現他所看到的行爲。

我想知道的是爲什麼GC在控制檯應用程序的Main方法中運行時似乎沒有按預期收集對象。通過提取方法,以便調用GC.Collect和超出範圍的對象以不同的方法發生,預期的行爲被恢復。

這些是我用來定義我的測試的對象。只有一個事件對象和一個爲事件處理程序提供合適方法的對象。兩者都有終結者設置一個全局變量,這樣你就可以知道他們什麼時候被收集了。

private static string Log; 
public const string EventedObjectDisposed = "EventedObject disposed"; 
public const string HandlingObjectDisposed = "HandlingObject disposed"; 

private class EventedObject 
{ 
    public event Action DoIt; 

    ~EventedObject() 
    { 
     Log = EventedObjectDisposed; 
    } 

    protected virtual void OnDoIt() 
    { 
     Action handler = DoIt; 
     if (handler != null) handler(); 
    } 
} 

private class HandlingObject 
{ 

    ~HandlingObject() 
    { 
     Log = HandlingObjectDisposed; 
    } 

    public void Yeah() 
    { 
    } 
} 

這是我測試(NUnit的),其通過:

[Test] 
public void TestReference() 
{ 
    { 
     HandlingObject subscriber = new HandlingObject(); 

     { 
      { 
       EventedObject publisher = new EventedObject(); 
       publisher.DoIt += subscriber.Yeah; 
      } 

      GC.Collect(GC.MaxGeneration); 
      GC.WaitForPendingFinalizers(); 
      Thread.MemoryBarrier(); 

      Assert.That(Log, Is.EqualTo(EventedObjectDisposed)); 
     } 

     //Assertion needed for foo reference, else optimization causes it to already be collected. 
     Assert.IsNotNull(subscriber); 
    } 

    GC.Collect(GC.MaxGeneration); 
    GC.WaitForPendingFinalizers(); 
    Thread.MemoryBarrier(); 

    Assert.That(Log, Is.EqualTo(HandlingObjectDisposed)); 
} 

我在一個新的控制檯應用程序的Main方法粘貼在主體的上方,並且被轉換的Assert調用Trace.Assert調用。兩個平等聲明失敗,然後失敗。如果你想要的話,結果Main方法的代碼是here

我確實認識到,GC發生時應該被視爲非確定性的,並且通常應用程序不應該關心其發生的時間。 在所有情況下,代碼都是以發佈模式編譯並針對.NET 4.5。

編輯:其他的事情我想

  • 使得測試方法static因爲NUnit的支持;測試仍然有效。
  • 我也嘗試將整個Main方法提取到程序中的實例方法並調用它。兩個斷言仍然失敗。
  • 歸屬Main[STAThread][MTAThread]在案件this作出了區別。兩個斷言仍然失敗。
  • 基於@武果汁的建議:
    • 我引用的NUnit的控制檯應用程序,這樣我可以使用NUnit的斷言,他們失敗了。
    • 我試着對測試,測試類,Main方法以及包含Main方法的靜態類進行各種更改。不用找了。
    • 我試着讓Test類是靜態的,而包含Main方法的類是靜態的。不用找了。
+0

當你使用帶默認參數的'GC.Collect()'並且不指定代時,結果是什麼? – 2013-04-21 15:57:39

+0

@ Moo-Juice測試依然通過,控制檯應用程序斷言仍然失敗,從控制檯應用程序中提取方法仍然會導致斷言成功。 – vossad01 2013-04-21 16:04:20

+0

我想知道這種差異是否是由於'Main'處於靜態類中的事實......我不確定,但除此之外,'Trace'與'Assert'的行爲我沒有想法。你是否嘗試過在靜態環境中運行測試? – 2013-04-21 16:21:45

回答

6

如果下面的代碼提取到一個單獨的方法,測試將更有可能表現爲你的預期。編輯:請注意,即使您將代碼提取到單獨的方法,C#語言規範的措辭也不要求此測試通過。

 { 
      EventedObject publisher = new EventedObject(); 
      publisher.DoIt += subscriber.Yeah; 
     } 

規範允許但不要求publisher有資格GC立即在該塊結束,所以你不應該在你所假設它可以在這裏收集這樣的方式編寫代碼。

編輯:從ECMA-334(C#語言規範)第10.9節自動存儲器管理(重點煤礦)

如果沒有對象的一部分可以通過任何可能的執行繼續進行訪問,除了終結者的運行,該對象被認爲不再被使用,並且它有資格完成。 [注:實現可能選擇分析代碼來確定將來可以使用哪些對象的引用。例如,如果範圍內的局部變量是對對象的唯一現有引用,但該過程中當前執行點的任何可能的繼續執行都不會引用該局部變量,則實現可能(但是不要求)將物體視爲不再使用。注完]

+0

爲了確保我理解,規範確實_require_'publisher'可用於GC,如果該代碼位在單獨的方法中,但不是如果它僅在由'{''}包圍的代碼塊內? – vossad01 2013-04-21 20:56:48

+0

@ vossad01是的,你是對的。但是,即使調用了GC.Collect,它也不會對運行終結器的時間有任何要求。因此,即使代碼被重新定位,測試也可能失敗,並且它不會指示C#編譯器或.NET實現中的錯誤。 – 2013-04-21 21:00:25

+0

'GC.WaitForPendingFinalizers'確保終結器運行,因爲GC.Collect沒有? – vossad01 2013-04-21 21:35:07

1

的問題不在於它是一個控制檯應用程序 - 問題是,你可能運行它通過Visual Studio - 附帶一個調試器!並且/或者您將控制檯應用編譯爲Debug版本。

確保您正在編譯發佈版本。然後轉到Debug -> Start Without Debugging,或按Ctrl + F5,或從命令行運行控制檯應用程序。垃圾收集器現在應該按預期行事。

這也是爲什麼Eric Lippert提醒您不要在調試器中運行任何性能基準的原因C# Performance Benchmark Mistakes, Part One

jit編譯器知道一個調試器已連接,並故意將其生成的代碼進行優化以使其更容易調試。垃圾收集器知道調試器已連接;它與jit編譯器一起工作,以確保內存清理不那麼積極,這可能會在某些情況下極大地影響性能。

Eric的一系列文章中的很多提示都適用於您的場景。如果您有興趣閱讀更多內容,請點擊以下鏈接查看部分two,threefour