如果未明确调用GC
,我遇到了Dispose()
未收集的可终结对象的问题。
我知道如果一个对象实现Dispose()
,我应该明确地调用IDisposable
,但我一直认为依赖框架是安全的,当一个对象变得未被引用时,它就可以被收集。
但是在使用windbg / sos / sosex进行一些实验后,我发现如果没有为可终结对象调用 GC.SuppressFinalize(),它就不会被收集,即使它没有被提取。因此,如果您广泛使用可终结对象(DbConnection,FileStream等)并且未明确处理它们,则可能会遇到过高的内存消耗甚至OutOfMemoryException
。
以下是一个示例应用程序:
public class MemoryTest
{
private HundredMegabyte hundred;
public void Run()
{
Console.WriteLine("ready to attach");
for (var i = 0; i < 100; i++)
{
Console.WriteLine("iteration #{0}", i + 1);
hundred = new HundredMegabyte();
Console.WriteLine("{0} object was initialized", hundred);
Console.ReadKey();
//hundred.Dispose();
hundred = null;
}
}
static void Main()
{
var test = new MemoryTest();
test.Run();
}
}
public class HundredMegabyte : IDisposable
{
private readonly Megabyte[] megabytes = new Megabyte[100];
public HundredMegabyte()
{
for (var i = 0; i < megabytes.Length; i++)
{
megabytes[i] = new Megabyte();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~HundredMegabyte()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
}
public override string ToString()
{
return String.Format("{0}MB", megabytes.Length);
}
}
public class Megabyte
{
private readonly Kilobyte[] kilobytes = new Kilobyte[1024];
public Megabyte()
{
for (var i = 0; i < kilobytes.Length; i++)
{
kilobytes[i] = new Kilobyte();
}
}
}
public class Kilobyte
{
private byte[] bytes = new byte[1024];
}
即使经过10次迭代,您仍然可以发现内存消耗过高(从700MB到1GB),并且随着迭代次数的增加会更高。使用WinDBG附加到进程后,您可以发现所有大对象都是无根,但未收集。
如果您明确调用SuppressFinalize()
,情况会发生变化:即使在高压下,内存消耗也会稳定在300-400MB左右,而WinDBG显示没有无根对象,内存是免费的。
所以问题是:它是框架中的错误吗?有没有合理的解释?
更多详情:
每次迭代后,windbg显示:
答案 0 :(得分:7)
具有终结器的对象的行为与缺少对象的对象的行为方式不同。
当发生GC并且尚未调用SuppressFinalize时,GC将无法收集实例,因为它必须执行Finalizer。因此,执行终结器,并将对象实例提升为第1代(在第一个GC中幸存的对象),即使它已经没有任何生命引用。
第1代(和Gen2)对象被认为是长期存在的,只有当Gen1 GC不足以释放足够的内存时才会考虑进行垃圾收集。我认为在测试过程中,Gen1 GC总是足够的。
这种行为对GC性能有影响,因为它否定了通过几个générations(你在gen1中有短持续时间对象)带来的优化。
基本上,拥有一个Finalizer并且无法阻止GC调用它将始终将已经死亡的对象提升为长寿堆,这不是一件好事。
因此,您应该正确处理您的IDisposable对象,如果没有必要,请避免使用Finalizer(如果需要,可以实现IDisposable并调用GC.SuppressFinalize。)
修改强> 我没有充分阅读代码示例:您的数据看起来像是存在于大对象堆(LOH)中,但事实上并非如此:您有很多小的引用数组包含在最后树小字节数组。
将短期对象放入LOH更糟糕,因为它们不会被压缩...因此,如果CLR无法找到空的内存段,则可以运行具有大量可用内存的OutOfMemory足以包含大量数据。
答案 1 :(得分:1)
我认为这背后的想法是,当您实现IDisposable时,这是因为您正在处理非托管资源,并且需要手动处理您的资源。
如果GC要调用Dispose或者试图摆脱它,它也会刷新非托管的东西,这很可能在其他地方使用,GC无法知道。
如果GC要删除无根对象,则会丢失对非托管资源的引用,这会导致内存泄漏。
所以......你被管理了,或者你没有。 GC没有办法处理未配置的IDisposables。