2010-04-27 121 views
18

當我執行一個AppDomain.Unload(myDomain)時,我希望它也做一個完整的垃圾收集。爲什麼調用AppDomain.Unload不會導致垃圾回收?

根據傑弗裏裏希特在「通過C#CLR」他說,一個AppDomain.Unload期間:

的CLR強制垃圾收集發生,回收由創建的任何對象 使用的內存現在卸載的AppDomain。調用這些對象的最終確定方法爲 ,使對象有機會正確清理自己。

據「史蒂芬Pratschner」中的「自定義.NET Framework公共語言運行庫」:

畢竟終結已經運行並沒有更多的線程在域中執行時,CLR準備卸載內部實現中使用的所有內存數據結構。但是,在此之前,必須收集駐留在域中的對象。在發生下一次垃圾回收後,將從進程地址空間卸載應用程序域數據結構,並認爲該域已卸載。

我在誤解他們的話嗎? 我做了以下的解決方案,以重現意外的行爲(在.NET 2.0 SP2):

稱爲「接口」包含此接口的類庫項目:

public interface IXmlClass 
    { 
     void AllocateMemory(int size); 

     void Collect(); 
    } 

一個類庫項目名爲「ClassLibrary1的」其中引用了「接口」,包含這個類:

public class XmlClass : MarshalByRefObject, IXmlClass 
{ 

    private byte[] b; 

    public void AllocateMemory(int size) 
    { 
     this.b = new byte[size]; 
    } 

    public void Collect() 
    { 
     Console.WriteLine("Call explicit GC.Collect() in " + AppDomain.CurrentDomain.FriendlyName + " Collect() method"); 
     GC.Collect(); 
     Console.WriteLine("Number of collections: Gen0:{0} Gen1:{1} Gen2:{2}", GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2)); 
    } 

    ~XmlClass() 
    { 
     Console.WriteLine("Finalizing in AppDomain {0}", AppDomain.CurrentDomain.FriendlyName); 
    } 
} 

A中引用的「接口」項目,並執行以下邏輯的控制檯應用程序項目:

static void Main(string[] args) 
{ 
    AssemblyName an = AssemblyName.GetAssemblyName("ClassLibrary1.dll"); 
    AppDomain appDomain2 = AppDomain.CreateDomain("MyDomain", null, AppDomain.CurrentDomain.SetupInformation); 
    IXmlClass c1 = (IXmlClass)appDomain2.CreateInstanceAndUnwrap(an.FullName, "ClassLibrary1.XmlClass"); 
    Console.WriteLine("Loaded Domain {0}", appDomain2.FriendlyName); 
    int tenmb = 1024 * 10000; 
    c1.AllocateMemory(tenmb); 
    Console.WriteLine("Number of collections: Gen0:{0} Gen1:{1} Gen2:{2}", GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2)); 
    c1.Collect(); 
    Console.WriteLine("Unloaded Domain{0}", appDomain2.FriendlyName); 
    AppDomain.Unload(appDomain2); 
    Console.WriteLine("Number of collections after unloading appdomain: Gen0:{0} Gen1:{1} Gen2:{2}", GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2)); 
    Console.WriteLine("Perform explicit GC.Collect() in Default Domain"); 
    GC.Collect(); 
    Console.WriteLine("Number of collections: Gen0:{0} Gen1:{1} Gen2:{2}", GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2)); 
    Console.ReadKey(); 
} 

運行控制檯應用程序時的輸出是:

Loaded Domain MyDomain 
Number of collections: Gen0:0 Gen1:0 Gen2:0 
Call explicit GC.Collect() in MyDomain Collect() method 
Number of collections: Gen0:1 Gen1:1 Gen2:1 
Unloaded Domain MyDomain 
Finalizing in AppDomain MyDomain 
Number of collections after unloading appdomain: Gen0:1 Gen1:1 Gen2:1 
Perform explicit GC.Collect() in Default Domain 
Number of collections: Gen0:2 Gen1:2 Gen2:2 

事情需要注意:

  1. 垃圾收集每個進程完成(只是回顧一下)

  2. 對象在卸載的appdomain中調用了終結器,但垃圾回收沒有完成。通過AllocateMemory(創建)的10兆字節的對象將僅在上述例子中執行的顯式GC.Collect的()(之後被收集,或者如果垃圾回收器會在一段時間後

其他說明:。它不「噸真的重要,如果XmlClass是終結或不出現在上面的例子同樣的行爲

問題:

  1. 爲什麼調用AppDomain.Unload不會導致垃圾回收?有沒有辦法讓這個調用導致垃圾回收?我打算加載短期大型XML文檔(小於或等於16 MB),它將在LargeObject堆上獲得,並且將成爲第2代對象。有沒有什麼辦法可以在不使用顯式GC.Collect()或其他類型的垃圾收集器的顯式程序控制的情況下收集內存?

回答

17

其他注意事項:

與傑弗裏裏希特一些郵件交換誰還跟看看這個問題後:

OK,看了你的帖子。
首先,在XMLClass對象爲GC'd之前,數組將不會GC'd,並且需要兩個GC來收集此對象,因爲它包含Finalize方法。
其次,卸載appdomain至少執行GC的標記階段,因爲這是確定哪些對象不可訪問以便可以調用它們的Finalize方法的唯一方法。
但是,卸載GC時可能會或可能不會完成GC的緊湊部分。調用GC.CollectionCount並不能說明整個故事。這並未顯示GC標記階段確實發生。
而且,AppDomain.Unload可能會通過一些內部代碼啓動GC,但不會導致收集計數變量增加。我們已經知道標記階段正在執行並且收集計數沒有反映這一點。

更好的測試是查看調試器中的某些對象地址並查看是否實際發生壓縮。如果它(並且我懷疑它),那麼收集計數只是沒有被正確更新。

如果你想發佈這個網站作爲我的迴應,你可以。

考慮他的意見,並尋找到SOS之後(也去掉了終結器),它揭示了這一點:

AppDomain.Unload前:

!EEHeap -gc 
Number of GC Heaps: 1 
generation 0 starts at 0x0180b1f0 
generation 1 starts at 0x017d100c 
generation 2 starts at 0x017d1000 
ephemeral segment allocation context: none 
segment begin allocated  size 
017d0000 017d1000 01811ff4 0x00040ff4(266228) 
Large object heap starts at 0x027d1000 
segment begin allocated  size 
027d0000 027d1000 02f75470 0x007a4470(8012912) 
Total Size 0x7e5464(8279140) 
------------------------------ 
GC Heap Size 0x7e5464(8279140) 

AppDomain.Unload後(同一地址,無堆壓縮完成)

!EEHeap -gc 
Number of GC Heaps: 1 
generation 0 starts at 0x0180b1f0 
generation 1 starts at 0x017d100c 
generation 2 starts at 0x017d1000 
ephemeral segment allocation context: none 
segment begin allocated  size 
017d0000 017d1000 01811ff4 0x00040ff4(266228) 
Large object heap starts at 0x027d1000 
segment begin allocated  size 
027d0000 027d1000 02f75470 0x007a4470(8012912) 
Total Size 0x7e5464(8279140) 
------------------------------ 
GC Heap Size 0x7e5464(8279140) 

在GC.Collect()之後,地址不同,表示堆壓縮已完成。

!EEHeap -gc 
Number of GC Heaps: 1 
generation 0 starts at 0x01811234 
generation 1 starts at 0x0180b1f0 
generation 2 starts at 0x017d1000 
ephemeral segment allocation context: none 
segment begin allocated  size 
017d0000 017d1000 01811ff4 0x00040ff4(266228) 
Large object heap starts at 0x027d1000 
segment begin allocated  size 
027d0000 027d1000 027d3240 0x00002240(8768) 
Total Size 0x43234(274996) 
------------------------------ 
GC Heap Size 0x43234(274996) 

經過更多的sos我得出的結論是,它肯定是由設計,堆壓實不一定完成。在AppDomain卸載過程中,唯一可以確定的是對象將被標記爲無法訪問,並且會在下次垃圾回收期間收集(就像我說的,它不會在您卸載應用程序域時完成,除非存在巧合)。

編輯:我也問Maoni Stephens,誰直接在GC小組工作。您可以在評論here的某處閱讀她的回覆。她確認這是設計。 案例關閉:)

5
  1. 大概在設計上,但我不明白你爲什麼要這種行爲(明確GC.Collect的)。只要調用終結器,就會從終結器隊列中移除對象,並準備好在需要時進行垃圾回收(gc線程將在必要時啓動)。

  2. 你或許可以使用一些令人討厭的非託管分配和一些沉重的互操作,或者在非託管C++中對其進行編碼,然後使用託管包裝來通過C#訪問它,但只要您保留在託管的.Net世界中, 。

    再次看看你的架構而不是專注於扮演垃圾收集器角色更明智。

+0

我完全同意你的看法。 – Steven 2010-04-27 18:21:10

+0

這可能是由設計,請參閱我的附加說明添加到問題。 – 2010-04-29 07:18:14