2010-04-27 63 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