【问题标题】:Why does calling AppDomain.Unload doesn't result in a garbage collection?为什么调用 AppDomain.Unload 不会导致垃圾回收?
【发布时间】:2011-02-12 23:51:01
【问题描述】:

当我执行 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);
    }
}

引用“接口”项目并执行以下逻辑的控制台应用程序项目:

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() 或其他类型的垃圾收集器显式编程控制的情况下收集内存?

【问题讨论】:

    标签: c# applicationdomain


    【解决方案1】:

    补充说明:

    在与杰弗里·里希特 (Jeffrey Richter) 进行了一些邮件交流后,他很友好地看了一下这个问题:

    好的,我看了你的帖子。
    首先,在 XMLClass 对象被 GC 之前,数组不会被 GC,并且它需要两次 GC 来收集这个对象,因为它包含一个 Finalize 方法。
    其次,卸载应用程序域至少会执行 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 卸载期间,您真正可以确定的唯一一件事是对象将被标记为不可访问,并将在下一次垃圾回收期间被收集(就像我说的那样,它不会在您卸载应用程序域时完成,除非有巧合)。

    编辑:我还询问了直接在 GC 团队工作的 Maoni Stephens。您可以在 cmets here 的某处阅读她的回复。她确认这是设计使然。 结案:)

    【讨论】:

      【解决方案2】:
      1. 可能是设计使然,但我不明白您为什么想要这种行为(显式 GC.Collect)。只要调用了终结器,对象就会从终结器队列中移除,并准备好在需要时进行垃圾回收(gc 线程将在必要时启动)。

      2. 您可能会使用一些令人讨厌的非托管分配和一些繁重的互操作,或者在非托管 c++ 中对其进行编码,然后使用托管包装器通过 C# 访问它,但只要您留在托管的 .Net 世界中,就不会。

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

      【讨论】:

      • 这可能是设计使然,请参阅我添加到问题中的附加说明。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-12-27
      • 1970-01-01
      • 1970-01-01
      • 2020-03-06
      • 1970-01-01
      • 1970-01-01
      • 2010-10-26
      相关资源
      最近更新 更多