为什么调用AppDomain.Unload不会导致垃圾回收?

当我执行AppDomain.Unload(myDomain)时,我希望它也可以执行完整的垃圾回收。

根据Jeffrey Richter在“CLR via C#”中的说法,他说在AppDomain.Unload期间:

CLR强制进行垃圾收集,回收由现在卸载的AppDomain创建的任何对象使用的内存。 调用这些对象的Finalize方法,使对象有机会正确地清理自己。

根据“自定义.NET Framework公共语言运行时”中的“Steven Pratschner”:

在所有终结器运行并且域中不再执行任何线程之后,CLR就可以卸载内部实现中使用的所有内存中数据结构。 但是,在此之前,必须收集驻留在域中的对象。 发生下一次垃圾收集后,将从进程地址空间卸载应用程序域数据结构,并将该域视为已卸载。

我误解了他们的话吗? 我做了以下解决方案来重现意外行为(在.net 2.0 sp2中):

包含此接口的名为“Interfaces”的类库项目:

public interface IXmlClass { void AllocateMemory(int size); void Collect(); } 

一个名为“ClassLibrary1”的类库项目,它引用了“Interfaces”并包含了这个类:

 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); } } 

一个控制台应用程序项目,它引用“Interfaces”项目并执行以下逻辑:

 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不会导致垃圾回收? 有没有办法在垃圾收集中调用该调用结果?

  2. 在AllocateMemory()内部,我计划加载将在LargeObject堆上获得并将成为第2代对象的短期大型xml文档(小于或等于16 mb)。 有没有办法收集内存而不采用显式GC.Collect()或其他类型的垃圾收集器的显式编程控制?

补充说明:

在与Jeffrey Richter进行一些邮件交换之后,他很友好地看了一下这个问题:

好的,我看了你的post。
首先,在XMLClass对象为GC之前,数组不会进行GC,并且需要两个GC来收集此对象,因为它包含Finalize方法。
其次,卸载appdomain至少执行GC的标记阶段,因为这是确定哪些对象不可达的唯一方法,以便可以调用它们的Finalize方法。
但是,卸载GC时可能会或可能不会执行GC的紧凑部分。 调用GC.CollectionCount显然并不能说明整个故事。 没有显示GC标记阶段确实发生过。
并且,AppDomain.Unload可能通过一些内部代码启动GC,这不会导致集合计数变量递增。 我们已经知道正在执行标记阶段并且收集计数没有反映这一点。

更好的测试是查看调试器中的一些对象地址,看看是否实际发生了压缩。 如果确实如此(我怀疑它确实如此),那么集合计数就没有正确更新。

如果您想将此post作为我的回复发布到网站,您可以。

在听完他的建议并调查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) 

在得到更多结论之后,我得出的结论是它肯定是设计的,并且堆压缩不一定完成。 在AppDomain卸载期间,您唯一可以确定的是,对象将被标记为无法访问,并将在下一次垃圾收集期间收集(就像我说的那样,当您卸载应用程序域时,它并没有完全完成,除非有巧合)。

编辑:我也问过直接在GC团队工作的Maoni Stephens。 您可以在评论中的某处阅读她的回复。 她确认这是设计的。 案例结束:)

  1. 可能是设计,但我不明白为什么你想要这种行为(显式GC.Collect)。 只要调用终结器,就会从终结器队列中删除对象,并且如果需要,可以进行垃圾收集(gc线程将在必要时启动)。

  2. 您可以使用一些令人讨厌的非托管分配和一些繁重的互操作,或者在非托管c ++中编码,然后使用托管包装器通过C#访问它,但只要您保持在托管的.Net世界中,否则。

    再看看你的架构而不是专注于尝试扮演垃圾收集器的角色更为明智。

    Interesting Posts