1

我发现 c# / xna 中的渲染目标不会自动释放,您必须调用 .dispose() 成员才能摆脱它们。

我认为垃圾收集应该在所有引用都消失后自动摆脱它们,这是什么?

还有什么不是自动处理的吗?

4

4 回答 4

10

我认为垃圾收集应该在所有引用都消失后自动摆脱它们,这是什么?

这在两个方面是不正确的。

垃圾收集收集可以安全收集和需要的托管内存。就是这样。

将垃圾收集视为模拟无限堆内存的一种方式。既然我们可以假装我们有无限的内存,我们就不必调用任何东西来释放我们用完的内存,因为我们为什么要保留无限的资源?*

GC 模拟无限堆的最简单方法是什么都不做。这在例如进程有 4GiB 可用内存并且它使用 50MiB 时有效。收集永远不会出现。如果应用程序足够小以至于永远不会发生集合,这确实会发生。(虽然它不会懒到让你在没有集合的情况下使用 megs,但它会在尝试向操作系统询问应用程序的更多内存之前尝试收集,但当你想知道“为什么不使用 GC”时它仍然很有用。 ..” 在某些时候什么都不做可能是一种有效的 GC 方法,一旦你想到了这种可能性,很多其他问题就会消失)。

另一种方法是在可以清理的那一刻立即清理所有东西。这发生在引用计数垃圾收集器上;这与 .NET 无关,但值得一提,因为它与您的问题的“在所有引用都消失后”非常匹配。

另一个是当需要内存,并且在已经空闲内存的存储中不可用时,GC 会停止所有线程,识别根(静态变量和每个线程堆栈中的本地引用),识别根引用的所有内容,以及那些引用的所有内容,依此类推,直到找到仍然可以被应用程序使用的所有内容,然后将其他所有内容占用的内存视为空闲。然后它会压缩所有没有释放内存的对象,这样既可以避免在释放更多可用内存时产生碎片,又可以使对象在内存中更紧密地结合在一起(这对性能的影响很小)。

如果还“提升”它没有删除的对象,因为如果第一次删除它是不正确的,那么下一次就不会了,所以它会更少地查看那些在这个过程中幸存下来的对象经常。

此时需要注意两点:

  1. 我们无法预测 GC 何时(如果有的话)会释放给定对象的内存。
  2. GC 唯一要做的就是释放托管内存。它不做任何其他事情(它确实有助于终结者,我们稍后会谈到)。

获取然后释放托管内存当然只是我们希望先做某事然后撤消它的一种情况。其他例子是:

  1. 获取文件句柄,并释放它。
  2. 获取一个窗口句柄,然后释放它。
  3. 获取 GDI 句柄,然后释放它。
  4. 打开网络连接,然后关闭它。
  5. 通过网络连接发送协议定义的握手,然后在关闭之前通过它发送协议定义的签名。
  6. 获取一些非托管内存,然后释放它。
  7. 从池中获取一个对象(对于该对象,除了分配给它的创建之外还有一些开销,或者它通过使用来“学习”),然后将其返回到池中。

到目前为止,我们所描述的 GC 对这些都没有帮助。

尽管如此,它们都有两个共同点。它们有一个开始操作和一个结束操作。

开始操作要么很好地映射到对象创建,要么映射到某个方法调用。

结束操作可以匹配Close(), End(), Free(),Release()方法调用,但在定义时IDisposable.Dispose()我们可以给它们一个通用接口。该语言还可以通过using†添加一些帮助。

(一个类可能同时具有 aClose()和 a Dispose()。在这种情况下,我们既可以选择关闭我们稍后将重新打开或以其他方式在其关闭状态下使用的东西,也可以选择在我们完成后确保清理的方法目的)。

因此,通过这种方式,IDisposable.Dispose()存在清理托管内存之外的所有需要​​清理的内容。

现在,在这种情况下,需要实现三种类型的类IDisposable

  1. 那些拥有像句柄这样的非托管资源的人,他们自己。
  2. 那些我们在我们自己的设计(或其他人的设计,但仍然在.NET 本身内)的某种池或其他场景之前/之后使用的那些。
  3. 那些具有依次实现的字段,IDisposable因此当我们处置此类的对象时,它会调用Dispose()这些字段。

让我们考虑一下如果 GC 释放了这样一个对象的内存并且Dispose()没有被调用会发生什么。

在第三种情况下,实际上没有处理对象并不重要。真正重要的是这些字段没有被处理(或者也许这无关紧要,但有些字段很重要)。

在第二种情况下,它的重要性取决于池的重要性。这可能不是最理想的,但不是世界末日。

在第一种情况下,这是一场灾难——我们有一个未释放的资源,在应用程序结束之前我们不可能释放它,甚至可能在应用程序结束之后(取决于资源的性质)。

出于这个原因,对象可以有终结器。

当 GC 即将释放一个对象的内存时,如果它有一个终结器,并且如果该终结器没有被抑制(Dispose()通常会这样做以表明该对象已被很好地清理并且不需要对其进行更多工作),然后不是从对象中释放内存,而是将其放入终结队列。这当然意味着该对象不仅没有收集到它的内存,而且也没有任何可以通过它的字段访问的对象。

终结器线程通过此队列工作,在每个队列上调用终结器方法。

发生这种情况有两个不好的地方:

  1. 我们不知道什么时候会发生。也许我们会用完资源或无法打开文件进行写入。
  2. 这意味着一个应该释放内存的对象被提升了,并且不仅会比它应该的寿命长一个周期,而且会长很多周期。

编辑:请注意,我们没有第三类课程的终结者,也许没有第二类课程。在这种情况下,不需要终结器,因为它作为字段具有的真正关键的对象将调用其终结器,它完成了重要的工作。如果您尝试从终结器中处理可终结的字段,也很容易得到一个严重的错误。如果您编写了一个一次性类,它包装了它“拥有”的一个或多个一次性字段并负责清理,那么请实现IDisposable,但不要添加终结器。

总之,调用 finalizer 意味着以下两种情况之一:

  1. 应用程序正在关闭,所有终结器都在运行(伟大的,一切都很好)。
  2. 有人搞砸了,没有清理应该清理的东西。

因此,虽然 GC 与除托管内存之外的资源之间通过终结器进行交互,但它是最后手段的交互,无论如何它都不可靠。您不应该将终结器视为使 GC 进行清理的一种方式,而是一种让 GC 在缺陷意味着它没有发生的情况下不让清理变得不可能的方式(以及一种清理的方式) -up 应用程序关闭)。

*当然,如果你认为你拥有无限的资源(鱼、水牛、海洋的废物处理能力)而事实证明你没有,那么事情就会变得一团糟,所以也许不要将这条规则应用于生活。

†<code>using 使调用Dispose()更简单

using(someDisposableObject)
{
  //Do Stuff
}

相当于:

try
{
  //Do Stuff
}
finally
{
  if(someDisposableObject != null)
    ((IDisposable)someDisposableObject).Dispose();
}

和:

using(var someDisposableObject = someMethodCallOrCallToNew())
{
  //Do Stuff
}

相当于:

var someDisposableObject = someMethodCallOrCallToNew();
try
{
  //Do Stuff
}
finally
{
  if(someDisposableObject != null)
    ((IDisposable)someDisposableObject).Dispose();
}

在编译器可以确定 someDisposableObject 不可能为空的情况下,可以删除空检查作为优化。

于 2012-08-13T15:09:26.093 回答
1

Dispose即使使用自动垃圾收集,当您确定不会再次使用该实例时调用它仍然是一个好主意。

或者使用using语句。

这样,您可以选择何时“处置”对象,而不是等到垃圾收集器执行此操作。

于 2012-08-13T14:23:36.127 回答
1

存在的原因IDisposable是支持垃圾收集无法[有效]管理对象内存的异常情况。它存在的主要原因是为了释放非托管资源。

这种情况的常见情况之一是与非托管内存的交互。当一个对象正在做一些涉及在垃圾收集器范围之外分配内存的事情时,垃圾收集器就不能负责清理它;它需要由程序员处理。这些情况也更有可能分配大量内存,因此确保更快地清理它变得更加重要。

它也用于对象需要进行某种“清理”而不是实际释放内存的情况。示例是处理 IO 时的文件处理程序,或与应关闭的数据库的连接。让垃圾收集器释放对象的内存不会执行这种类型的清理。

在某些情况下,程序员使用IDisposeable接口“劫持”using语句的语法,实际上并没有在它的 dispose 方法中处理非托管资源。

于 2012-08-13T14:34:42.917 回答
0

我的观点是,如果一个对象有一个 Dispose 方法,它可能需要被释放。我通常尝试将它放在一个带有“使用”语句的块中。这将在块的末尾自动调用 Dispose 方法。

于 2012-08-13T14:25:39.357 回答