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

sha*_*oth 2 .net c# 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;
}
Run Code Online (Sandbox Code Playgroud)

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

var wrapper = CoolWrapper();
wrapper.DoSomething();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

var wrapper = CoolWrapper();
wrapper.DoSomething();
GC.KeepAlive(wrapper);
Run Code Online (Sandbox Code Playgroud)

这使得该对象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
     }
 }
Run Code Online (Sandbox Code Playgroud)

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

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

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

Jon*_*eet 5

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

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

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

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