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

21

当我执行 AppDomain.Unload(myDomain) 时,我期望它也会进行完整的垃圾回收。

根据 Jeffrey Richter 在《CLR via C#》一书中的说法,在 AppDomain.Unload期间:

CLR 强制要求进行垃圾回收,以回收由已卸载的 AppDomain 创建的任何对象使用的内存。对于这些对象,将调用 Finalize 方法,使得这些对象有机会适当地清理自身。

根据 "Steven Pratschner" 在 "Customizing .NET Framework Common Language Runtime" 中的说法:

在所有终结器运行并且域中不再有更多线程正在执行之后,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. 在卸载应用程序域中的对象时,会调用终结器但不会进行垃圾回收。上述示例中通过AllocateMemory()创建的10兆字节的对象只有在执行显式的GC.Collect()(或者垃圾回收器在以后某个时间内执行)后才能被回收。

其他注释:XmlClass是否可终结化并不重要。在上面的示例中,发生相同的行为。

问题:

  1. 为什么调用AppDomain.Unload不会导致垃圾收集?有没有办法使该调用导致垃圾收集?

  2. 在AllocateMemory()内部,我计划加载短暂的大型XML文档(小于等于16 MB),这些文档将放在LargeObject堆上,并且将成为第2代对象。有没有办法在不使用显式的GC.Collect()或其他形式的垃圾收集器控制的情况下对其进行内存回收?

2个回答

20

附加说明:

在与善良的Jeffrey Richter进行了一些电子邮件交流之后,他很好地查看了这个问题:

好的,我已经阅读了您的帖子。
首先,仅当XMLClass对象被GC收集时,数组才不会被GC回收,而且需要两个GC才能收集此对象,因为它包含Finalize方法。
其次,卸载appdomain至少执行GC的标记阶段,因为这是确定哪些对象是不可达的并调用其Finalize方法的唯一方法。
但是,GC的压缩部分可能会或可能不会在卸载GC时完成。调用GC.CollectionCount显然不能说明全部情况。 它没有显示GC标记阶段已发生。
而且,AppDomain.Unload有可能通过某些内部代码启动GC,而不导致增加收集计数变量。我们已经确切知道正在执行标记阶段,而收集计数并未反映这一点。

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

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

在采纳他的建议并使用SOS(还删除了finalizer)之后,它显示了以下内容:

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)
更多的研究之后,我得出的结论是这是设计上的考虑,并且堆压缩并不一定会发生。在应用程序域卸载期间唯一可以确定的事情是对象将被标记为不可达,并将在下一次垃圾回收期间进行收集(正如我所说,除非有巧合,否则垃圾回收并不会在您卸载应用程序域时立即执行)。
编辑:我还向直接在GC团队工作的Maoni Stephens提问,你可以在这里的评论中阅读她的回答。她证实这是按设计要求执行的。案子结束了 :)

5
  1. 可能是因为设计上的原因,但我不明白为什么您需要这种行为(显式GC.Collect)。只要调用了终结器,对象就会从终结器队列中删除,如果需要的话,就准备好进行垃圾回收(gc线程在必要时会启动)。

  2. 您可以尝试使用一些令人讨厌的非托管分配和重度互操作性,或者使用非托管c ++编写代码,然后使用托管的包装器通过C#进行访问。但是,只要保持在托管的.NET世界中,是不需要这样做的。

    与其专注于尝试扮演垃圾收集器的角色,倒不如重新审视您的架构。


很可能是设计上的,可以看一下我在问题中添加的附加说明。 - Liviu Trifoi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接