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

时间:2010-04-27 17:43:34

标签: c# applicationdomain

当我执行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()之后才会被收集(或者如果垃圾收集器将在以后的某个时间执行。

  3. 其他注意事项:XmlClass是否可以终结并不重要。上例中出现相同的行为。

    问题:

    1. 为什么调用AppDomain.Unload不会导致垃圾回收?有没有办法在垃圾收集中调用该调用结果?

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

2 个答案:

答案 0 :(得分:17)

附加说明:

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

  

好的,我看了你的帖子   首先,在XMLClass对象为GC并且需要两个GC来收集此对象之前,数组将不会进行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)

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

编辑:我还问过直接在GC团队工作的Maoni Stephens。您可以在评论here中的某处阅读她的回复。她证实这是设计上的。 案件结束:)

答案 1 :(得分:5)

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

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

    再次审视您的架构而不是专注于尝试扮演垃圾收集器更为明智。