当我执行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
需要注意的事项:
垃圾收集按流程完成(只是复习)
appdomain中被卸载的对象调用了终结器但是没有完成垃圾收集。 AllocateMemory()创建的10兆字节对象只有在上面的示例中执行显式GC.Collect()之后才会被收集(或者如果垃圾收集器将在以后的某个时间执行。
其他注意事项:XmlClass是否可以终结并不重要。上例中出现相同的行为。
问题:
为什么调用AppDomain.Unload不会导致垃圾回收?有没有办法在垃圾收集中调用该调用结果?
在AllocateMemory()内部我计划加载将在LargeObject堆上获得的短期大型xml文档(小于或等于16 mb)并将成为第2代对象。有没有办法收集内存而不采用显式GC.Collect()或其他类型的垃圾收集器的显式编程控制?
答案 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)
可能是设计,但我不明白你为什么要这种行为(显式GC.Collect)。只要调用终结器,就会从终结器队列中删除对象,并且如果需要,可以进行垃圾收集(gc线程将在必要时启动)。
您可以使用一些讨厌的非托管分配和一些繁重的互操作,或者在非托管c ++中编码,然后使用托管包装器通过C#访问它,但只要您保持在托管的.Net世界中,号
再次审视您的架构而不是专注于尝试扮演垃圾收集器更为明智。