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

Liv*_*foi 20 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();
    }
Run Code Online (Sandbox Code Playgroud)

一个名为"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);
    }
}
Run Code Online (Sandbox Code Playgroud)

一个控制台应用程序项目,它引用了"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();
}
Run Code Online (Sandbox Code Playgroud)

运行控制台应用程序时的输出是:

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
Run Code Online (Sandbox Code Playgroud)

需要注意的事项:

  1. 每个进程完成垃圾收集(只是复习)

  2. appdomain中被卸载的对象调用了终结器,但是没有完成垃圾收集.AllocateMemory()创建的10兆字节对象只有在上面的示例中执行显式GC.Collect()之后才会被收集(或者如果垃圾收集器将在稍后的某个时间收集.

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

问题:

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

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

Liv*_*foi 18

补充说明:

在与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)
Run Code Online (Sandbox Code Playgroud)

在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)
Run Code Online (Sandbox Code Playgroud)

在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)
Run Code Online (Sandbox Code Playgroud)

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

编辑:我也问过直接在GC团队工作的Maoni Stephens.您可以在评论的某处看过她的反应在这里.她证实这是设计上的.案例结束:)


Mar*_*ius 5

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

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

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