为什么允许垃圾收集器使用终结器收集看似引用的对象?

时间:2017-12-05 14:52:28

标签: c# .net garbage-collection

这个问题基本上就是为什么我们首先需要GC.KeepAlive()

这就是我们需要它的地方。我们有一些非托管资源的包装器

public class CoolWrapper
{
     public CoolWrapper()
     {
         coolResourceHandle = UnmanagedWinApiCode.CreateCoolResource();
         if (coolResourceHandle == IntPtr.Zero)
         {
             // something went wrong, throw exception
         }
     }

     ~CoolWrapper()
     {
         UnmanagedWinApiCode.DestroyCoolResource(coolResource);
     }

     public void DoSomething()
     {
         var result = UnmanagedWinApiCode.DoSomething(coolResource);
         if (result == 0)
         {
             // something went wrong, throw exception
         }
     }

     private IntPtr coolResourceHandle;
}

我们的代码使用该包装器:

var wrapper = CoolWrapper();
wrapper.DoSomething();

如果此代码在 Release 配置中运行而不是在调试器下运行,那么代码优化器可能会发现在此代码之后实际上未使用该引用,并且coolResourceHandleDoSomething()内部读取成员变量后,不会访问该成员变量(通过托管代码),并将其值传递给非托管代码,因此会发生以下情况:

  • DoSomething()被称为
  • coolResourceHandle正在阅读
  • 垃圾收集突然开始
  • ~CoolWrapper()运行
  • UnmanagedWinApiCode.DestroyCoolResource()运行,资源被破坏,资源句柄无效
  • UnmanagedWinApiCode.DoSomething()使用现在引用不存在的对象的值运行(或者可能创建另一个对象并分配该句柄)

上面描述的情况实际上是可能的,它是对象的方法和正在运行的垃圾收集之间的竞争。无论堆栈上是否存在引用类型的局部变量 - 优化后的代码都会忽略该引用,并且在coolResourceHandle中读取DoSomething()后,该对象立即有资格进行垃圾回收。

因此,为了防止这种情况,我们使用GC.KeepAlive()

var wrapper = CoolWrapper();
wrapper.DoSomething();
GC.KeepAlive(wrapper);

,在调用GC.KeepAlive()之前,该对象不符合GC的条件。

这当然要求所有用户在任何地方都会使用GC.KeepAlive(),因此正确的位置是CoolWrapper.DoSomething()

 public void DoSomething()
 {
     var result = UnmanagedWinApiCode.DoSomething(coolResource);
     GC.KeepAlive(this);
     if (result == 0)
     {
         // something went wrong, throw exception
     }
 }

这基本上可以防止对象在运行此对象的方法时获得GC的条件。

为什么需要这个?为什么GC不会忽略当时运行方法并且还有终结器的对象?这会让生活变得更轻松,但我们需要使用GC.KeepAlive()

为什么允许这样的积极收集而不是忽略具有当前正在运行的方法和终结器的对象(如果有如上所述的竞赛,可能会出现问题)?

3 个答案:

答案 0 :(得分:5)

  

为什么需要这个?为什么GC不会忽略当时运行方法并且还有终结器的对象?

因为这不是GC(或C#规范)所保证的。保证是如果一个对象无法完成或收集,同时仍然可以从中读取一个字段。如果JIT / GC检测到虽然您当前正在执行实例方法,但是没有执行路径,该方法将读取更多字段,对于要收集的对象是合法的(假设没有其他任何东西保持它活着)。 / p>

这是令人惊讶的,但这是规则 - 我强烈怀疑它的原因是允许优化路径否则是不可能的。

使用GC.KeepAlive的修复是完全合理的。请注意,相关的情况非常少。

答案 1 :(得分:2)

终结者不要"收集"任何东西。相反,它们会阻止收集对象并通知对象将被收集但是存在活动的终结器。请注意,如果对象X保存对Y的引用,则如果 X或Y具有活动终结器,则Y将无法收集。 Y的终结者(如果存在的话)将无法知道它是否是唯一保持Y活着的东西,或者是否存在其他终结者也会使Y保持活着。

一个基本原则是,只要对象存在于任何地方,对象就存在;一旦对象的最后一次引用不再存在,该对象也将如此。 GC不会破坏物体;相反,它回收了以前由不再存在的对象使用的内存。如果一个对象有一个活动的终结器,对它的引用将保存在具有活动终结器的特殊对象列表中;只要该引用存在,该对象也将这样做。执行GC时,系统会标记即使在没有该列表的情况下也会存在的所有对象,并且一旦完成,它就会生成该列表上的对象队列但尚未标记。之后,它将开始调用该队列上对象的终结器。

答案 2 :(得分:1)

考虑任何创建垃圾的方法,然后在退出之前花费很长时间做其他事情。显而易见的例子是任何可执行文件的main方法,它可以在进入某种形式的循环(例如窗口消息循环)之前执行任意数量的初始化操作,该循环不会在进程的整个生命周期内退出。 / p>

我们希望能够清理垃圾。但这意味着我们必须允许GC不将方法视为不透明 - 它必须能够检查正在运行的方法并且知道此时仍在使用的并且仅保护这些项目被收集。

这就是为什么GC是“积极的”以及为什么对象收集可以随时发生 - 即使在构造函数仍在运行时(假设它不会从其执行的当前点访问任何实例成员)。