从.NET GC的角度来看,在非托管代码中导致过早收集的事件序列是什么?

时间:2013-09-11 11:29:21

标签: .net garbage-collection

我真的很难掌握.NET中托管/非托管代码之间过早的垃圾收集,我想知道这里是否有人有一个很好的解释方法。

我们遇到的错误类似于此处描述的错误: http://www.codeproject.com/Tips/246372/Premature-NET-garbage-collection-or-Dude-wheres-my

基本上我们在一个调用非托管代码的对象上调用一个方法;支持它的托管对象获得GC'd并调用其终结器;需要GC.KeepAlive来阻止这种情况发生:

(取自链接文章的代码):

Foo a = new Foo();
while (true)
{
    FooBar b = new FooBar();
    b.WorkWith(a);
    GC.KeepAlive(b);
}

现在,我知道GC.KeepAlive是必需的,但我不明白GC如何得出结论b可以在没有KeepAlive的情况下被丢弃。运行时是否知道b正在执行一个方法,即使它是一个进入本机代码的方法(特别是'b'在方法调用中被用作此引用)?

为什么'b'在进入WorkWith(..)时有资格进行收集?为什么垃圾收集器不会假设在退出WorkWith方法时可以使'b'符合条件?

我错过了什么?这实际上是如何发挥作用的?

更新感谢大家的回答。我觉得我现在明白了这一点。仍然试图为我们的特定API找到一个很好的解决方案,但我想我会留下一个单独的问题:)

2 个答案:

答案 0 :(得分:3)

抖动生成一个内部表,描述存储局部变量的位置以及它们何时开始和停止存储对象引用。该表将说明b变量在WorkWith调用时停止相关。它生成了 this 参数,该参数传递给实例方法,之后不再使用,包括方法调用本身。

现在由WorkWith()方法跟踪对象引用的用法。如果该方法实际上是在本机代码中实现的,那么很可能会出现问题,因此没有表格来描述何时仍然相关。

因此,{/ 1}}对象可以在本机代码运行时进行垃圾回收。当程序中的另一个线程触发GC时会发生这种情况。

GC.KeepAlive()调用修改该表,将其扩展到方法调用之后。我们无法看到的代码最终存在缺陷,可能是某些与本机代码交互的C ++ / CLI代码。它应该 代码来处理引用。这通常是偶然的,使用GC.KeepAlive()是这种错误的有效解决方法。

您可以联系此组件的所有者,并建议他使用gcManagedToUnmanaged debugger assistant清除此类错误。它的工作原理是在转换为非托管代码时故意强制执行垃圾收集。

答案 1 :(得分:0)

虽然虚拟方法调度稍微复杂一些,但实例方法的行为很像静态方法,其中this作为第一个参数传递。因此,在课程Foo中,具有字段int Bar的方法

void SetBar(int newBar) { Bar = newBar; }

在内部等同于:

static void SetBar(Foo This, int newBar) { This.Bar = newBar; }

如果Foo是一个名为MyBitmap的字段,该字段对非托管位图持有IntPtr句柄,并且具有如下方法:

// Should call GC.SuppressFinalize, but doesn't.
static Byte[] ExportBitmapDataAndDispose(Foo This)
{
  IntPtr myBits = This.MyBitMap;
  if (myBits == 0) throw new ObjectDisposedException(...);
  int destSize = ExternalBitmapHandler.GetSize(myBits);
  var result = new Byte[destSize];
  ExternalBitmapHandler.CopyBits(myBits, ref result[0], destSize);
  ExternalBitmapHandler.ReleaseBits(myBits);
  myBits = 0;
}

编译器会在从中读取字段This后看到MyBitMap从未使用过。代码仍然需要由myBits标识的外部资源,但垃圾收集器对此类事物一无所知。从它的角度来看,如果This引用的对象没有其他实时引用,则代码不应该关心那些对象是否在那时根本不存在或者保持更长时间。实际上,它的假设是正确的,因为运行代码确实不关心这些对象何时不复存在。不幸的是,在终结器存在的情况下,物体不会停止存在。相反,如果垃圾收集器注意到Finalization Queue拥有This的唯一实时引用,它可能会运行This.Finalize(),而ExternalBitmapHandler可能会通知myBits This标识的位图不再需要1}}。

请注意,问题实际上不是This垃圾收集,而是ExternalBitmapHandler Finalization 。最终确定是垃圾收集,而是当GC发现一个有资格立即消灭的对象时触发的操作,如果不是注册的终结者。查看它的另一种方法是,当This.MyBitMap标识的位图不再需要时,应该通知This,但垃圾收集器唯一可以告诉的是{{1}不再需要。一旦代码已将This.MyBitMap读入myBits,它就不再需要This,如果This不再存在,我们会非常高兴,只要它没有破坏引用的位图通过`myBits。代码的替代版本可能是:

static Byte[] ExportBitmapDataAndDispose(Foo This)
{
  IntPtr myBits = System.Threading.Interlocked.Exchange(ref This.MyBitMap, 0);
  GC.SuppressFinalize(This);
  if (myBits == 0) throw new ObjectDisposedException(...);

  int destSize = ExternalBitmapHandler.GetSize(myBits);
  try
  {
    var result = new Byte[destSize]; // Could throw OutOfMemoryException
    ExternalBitmapHandler.CopyBits(myBits, ref result[0], destSize);
  }
  finally
  {
    ExternalBitmapHandler.ReleaseBits(myBits);
  }
}

请注意,在这种情况下,没有KeepAliveSuppressFinalize代码也没有放在最后,它会推迟任何完成的可能性,直到一切都完成。一旦Interlocked.Exchange发生,系统破坏This就没有错,因为它不再需要任何东西;系统需要保持足够长的时间以执行SuppressFinalize,但之后它可能会消失,没有人会注意到。