什么时候强制垃圾回收是个好主意?


135

因此,我正在阅读一个有关强制C#垃圾收集器在几乎每个答案都相同的地方运行的问题:您可以做到,但您不应该这样做-除了一些非常罕见的情况。可悲的是,那里没有人详细说明这种情况。

您能告诉我在哪种情况下强制垃圾回收实际上是一个好主意吗?

我不是在询问C#的特定情况,而是询问所有具有垃圾收集器的编程语言。我知道您不能在所有语言(例如Java)上强制使用GC,但让我们假设可以。


17
“但是,所有具有垃圾收集器的编程语言”不同的语言(或更恰当地说,是不同的实现)使用不同的方法进行垃圾收集,因此您不太可能找到一种“一刀切”的规则。
上校32年

4
@Doval如果您处于实时限制之下,并且GC无法提供匹配的保证,那么您将身处困境。与不执行任何操作相比,它可以减少不必要的暂停,但是据我所知,避免在正常操作过程中进行分配“比较容易”。

3
我给人的印象是,如果您期望有严格的实时截止日期,那么您永远不会首先使用GC语言。
GregRos

4
我看不到如何以非特定于VM的方式回答此问题。与32位进程相关,与64位进程无关。.NET JVM用于高端一个
rwong

3
@DavidConrad,您可以在C#中强制使用。因此是一个问题。
欧米茄

Answers:


127

您真的无法就使用所有 GC实现的适当方式发表笼统的声明。他们千差万别。因此,我将与您最初提到的.NET进行交流。

您必须非常清楚地了解GC的行为,才能以任何逻辑或理由进行操作。

关于收集的唯一建议是:永远不要这样做。

如果您真正了解GC的复杂细节,则不需要我的建议,因此无关紧要。如果您还没有百分百的信心,它将对您有所帮助,并且必须上网查找以下答案:您不应该致电GC.Collect,或者:您应该学习有关GC工作原理的详细信息由内而外,只有这样,您才知道答案

有一个安全的地方使用GC.Collect是有意义的

GC.Collect是一个可用的API,可用于对事物的时间进行概要分析。您可以知道一种算法,然后收集并分析另一种算法,然后知道在第二种算法导致结果倾斜时没有发生第一种算法的GC。

这种分析是我一次建议手动收集给任何人的一次。


还是人为的例子

一种可能的用例是,如果您加载非常大的东西,它们将最终进入大型对象堆,该对象将直接进入第2代,尽管第2代同样适用于寿命长的对象,因为它的收集频率较低。如果您知道出于任何原因将短命的对象加载到Gen 2中,则可以更快地清除它们,以使Gen 2变得更小,并且集合速度更快。

这是我能提出的最好的例子,而且不好-您在这里建立的LOH压力将导致更频繁的收集,并且收集是如此频繁-可能会像清除LOH一样当您用临时物体将其吹灭时,速度很快。我简直不相信自己推定比GC本身更好的采集频率-被人们远远调整远远比我更聪明


因此,让我们谈谈.NET GC中的一些语义和机制...或..

知道的有关.NET GC的一切

请任何在这里发现错误的人-纠正我。众所周知,许多GC都是黑魔法,尽管我尝试遗漏了不确定的细节,但我可能还是有些错误。

下面故意遗漏了许多我不确定的细节,以及我根本不了解的大量信息。使用此信息的风险自负。


GC概念

.NET GC发生在不一致的时间,这就是为什么它被称为“不确定”的原因,这意味着您不能依赖它在特定时间发生。这也是一个世代垃圾收集器,这意味着它将对象划分为它们经历了多少次GC传递。

Gen 0堆中的对象已经历0个集合,这些对象是新创建的,因此自实例化以来,最近没有发生任何集合。Gen 1堆中的对象已通过一次收集过程,同样,Gen 2堆中的对象也已通过两次收集过程。

现在,值得注意的是它可以限定这些特定世代和分区的原因。.NET GC只识别这三代,因为遍历这三个堆的收集传递都略有不同。有些对象可能在数千次收集中幸存下来。GC只是将它们留在了Gen 2堆分区的另一侧,没有必要将它们进一步分区,因为它们实际上是Gen 44。传递给它们的集合与第二代堆中的所有东西相同。

这些特定的世代都有语义上的目的,以及实现这些目标的已实现机制,我将在稍后进行介绍。


收藏中有什么

GC收集通道的基本概念是,它检查堆空间中的每个对象,以查看是否仍然存在对这些对象的实时引用(GC根)。如果为某个对象找到了GC根目录,则表示当前仍可以访问并使用该对象,因此无法将其删除。但是,如果找不到某个对象的GC根目录,则意味着正在运行的进程不再需要该对象,因此可以将其删除以为新对象释放内存。

现在,清理完一堆物体并将其搁置后,将产生一个不幸的副作用:已移除死者的活动物体之间的自由空间间隙。如果仅留下这种内存碎片,只会浪费内存,因此集合通常会执行所谓的“压缩”,将所有剩余的活动对象取出并挤入堆中,因此对于Gen来说,空闲内存在堆的一侧是连续的0。

现在给出了3个内存堆的概念,所有内存都按它们经历的收集遍数进行了分区,让我们讨论为什么存在这些分区。


Gen 0系列

Gen 0是绝对最新的对象,它通常很小-因此您可以安全地频繁收集它。频率确保堆保持很小并且收集非常快,因为它们是在如此小的堆上进行收集。这或多或少地基于一种启发式的主张:您创建的绝大多数临时对象是非常临时的,因此临时的对象将几乎不再在使用后立即使用或引用,因此可以被收集。


第一代收藏

1名是对象没有落入这个非常对象的临时类别,可能仍然是相当短暂的,因为still-创建对象的一个巨大的一部分长期不使用。因此,第1代也相当频繁地进行收集,再次使其堆很小,因此收集速度很快。然而,假设是比一代0它的对象是临时的,所以它比一代0收集不那么频繁

我要说的是,坦率地说,我不知道Gen 0的收集通行证和Gen 1的收集通行证之间的技术机制是否不同,除了他们收集的频率以外。


第2代收藏

现在第二代一定是所有堆的母亲吧?好吧,是的,这或多或少是对的。它是所有永久对象的住所- Main()例如,您生活的对象以及所有Main()引用的对象,因为这些对象将被植根,直到您Main()在过程结束时返回为止。

鉴于Gen 2基本上是其他世代无法收集的所有东西的存储桶,因此它的对象在很大程度上是永久性的,或者至少寿命长。因此,认识到Gen 2中几乎没有什么东西实际上是可以收集的东西,不需要经常收集。这使得它的收集也变慢,因为它执行的频率要少得多。因此,基本上,这是他们在奇怪的情况下处理所有额外行为的地方,因为他们有时间执行它们。


大对象堆

第2代的额外行为的一个例子是,它不会在大对象堆收集。到目前为止,我一直在谈论小型对象堆,但是由于上面我所说的压缩,.NET运行时将某些大小的东西分配给一个单独的堆。压缩需要在小型对象堆上完成收集时移动对象。如果第1代中有一个10mb的活物,则收集之后完成压缩将花费更长的时间,从而减慢了第1代的收集速度。因此,将10mb的对象分配给大对象堆,并在第2代中收集了该对象,因此运行很少。


定案

另一个示例是带有终结器的对象。您将终结器放在引用了.NETs GC范围以外的资源(非托管资源)的对象上。终结器是GC要求收集非托管资源的唯一方法-您实现终结器以手动收集/删除/释放非托管资源以确保它不会从您的流程中泄漏。当GC开始执行对象终结器时,您的实现将清除非托管资源,使GC能够删除您的对象而不会冒资源泄漏的风险。

终结器执行此操作的机制是直接在终结队列中引用。当运行时使用终结器分配对象时,它会将指向该对象的指针添加到终结队列中,并将对象锁定在适当的位置(称为固定),因此压缩不会移动它,这会破坏终结队列引用。随着收集过程的进行,最终将发现您的对象不再具有GC根目录,但是必须执行终结处理才能收集对象。因此,当对象失效时,集合将把它的引用从终结队列中移出,并将对其的引用放置在所谓的“可扩展”队列中。然后收集继续进行。在未来的另一个“不确定性”时刻,称为终结器线程的单独线程将通过FReachable队列,为每个引用的对象执行终结器。完成后,FReachable队列为空,并且已在每个对象的标头上翻转了一点,即它们不需要终结(也可以使用GC.SuppressFinalize(这在Dispose()方法中很常见),我也怀疑它已取消固定对象,但在此未引用我。该对象所在的任何堆上出现的下一个收集最终将收集它。Gen 0集合甚至不关注那些需要完成终结的对象,它会自动升级它们,甚至不检查其根。在Gen 1中需要终结的无根对象将被扔到FReachable队列中,但是集合对它没有做任何其他事情,因此它进入Gen2。这样,所有具有终结器且没有终结器的对象GC.SuppressFinalize将在第二代中收集。


4
@FlorianMargaine啊...说关于“GC”任何在所有的实现真的没有意义..
吉米·霍法

10
tl; dr:改为使用对象池。
罗伯特·哈维

5
tl; dr:对于定时/性能分析,它可能很有用。
kutschkem 2015年

3
在阅读完我上面对力学的描述(据我了解)之后,@ Den,您看到的好处是什么?您清理了许多对象-在SOH(或LOH?)中?您是否只是让其他线程暂停此集合?该收集是否仅将清除后的第二代对象提升了两倍?收集是否导致LOH压缩(您已将其打开?)?您有多少个GC堆,并且GC是在服务器还是台式机模式下?GC是一个冰冷的冰山,奸诈在海底。只是避开。我不够聪明,无法舒适地收集东西。
Jimmy Hoffa 2015年

4
@RobertHarvey对象池也不是灵丹妙药。垃圾收集器的第0代实际上已经是一个对象池-通常将其大小设置为适合最小的缓存级别,因此通常在已经在缓存中的内存中创建新对象。您的对象池现在正在与GC的托儿所争夺缓存,并且如果GC的托儿所和您的池的总和大于缓存,则您显然会丢失缓存。而且,如果您现在计划使用并行性,则必须重新实现同步,并担心错误共享。
2015年

68

可悲的是,那里没有人详细说明这种情况。

我会举一些例子。总而言之,强制使用GC是个好主意,但这是完全值得的。这个答案是根据我对.NET和GC文献的经验得出的。它应该很好地推广到其他平台(至少具有重要GC的平台)。

  • 各种基准。您需要基准测试开始时的已知托管堆状态,以便GC在基准测试期间不会随机触发。当您重复执行基准测试时,您希望每次重复中具有相同数量和数量的GC工作。
  • 突然释放资源。例如,关闭重要的GUI窗口或刷新缓存(从而释放旧的可能很大的缓存内容)。GC无法检测到这一点,因为您所做的只是将引用设置为null。它孤立了整个对象图的事实不容易检测到。
  • 释放泄漏非托管资源。当然,这永远都不会发生,但是我已经看到了第三方库泄漏内容(例如COM对象)的情况。开发人员有时被迫进行收集。
  • 互动应用程序,例如游戏。在进行游戏时,每帧的时间预算非常严格(60Hz => 16ms每帧)。为了避免出现问题,您需要一种应对GC的策略。一种这样的策略是尽可能延迟G2 GC,并在适当的时间(例如加载屏幕或剪辑场景)强制使用它们。GC无法确定何时是最佳的时刻。
  • 一般的延迟控制。某些Web应用程序会禁用GC,并在从负载均衡器旋转中退出时定期运行G2收集。这样,G2延迟就不会对用户浮出水面。

如果您的目标是吞吐量,那么GC越稀有越好。在这些情况下,强制收集不会产生积极的影响(人为设计的问题除外,例如通过删除散布在活动对象中的无效对象来提高CPU缓存利用率)。对于我认识的所有收集者而言,批次收集效率更高。对于稳定状态下的生产应用程序,诱导GC不会消耗内存。

上面给出的示例以内存使用的一致性和有界性为目标。在那些情况下,诱导GC可能很有意义。

似乎有一个广泛的想法,认为GC是一种神圣的实体,只要确实做到这一点,就会诱使收集。我所知道的任何GC都不是那么复杂,对于GC来说,要实现最佳性能确实很难。GC比开发人员了解的少。它的启发式方法基于内存计数器以及诸如收集率之类的东西。启发式方法通常很好,但是它们不能捕获应用程序行为的突然变化,例如释放大量托管内存。它还对非托管资源和延迟要求视而不见。

请注意,GC成本随堆大小和堆上引用的数量而变化。在很小的堆上,成本可能很小。我已经在具有1GB堆大小的生产应用程序中看到了1-2GB /秒的.NET 4.5的G2收集速率。


对于延迟控制的情况,我想不是定期执行此操作,您也可以根据需要执行此操作(即,当内存使用量超过特定阈值时)。
圣保罗Ebermann

3
倒数第二段为+1。有些人对编译器有相同的看法,并很快将几乎所有内容称为“过早优化”。我通常会告诉他们类似的事情。
Honza Brabec 2015年

2
对该段落也+1。我感到震惊的是,人们认为别人编写的计算机程序必须比自己更好地理解其程序的性能特征。
Mehrdad 2015年

1
@HonzaBrabec在两种情况下,问题都是相同的:如果您认为自己比GC或编译器了解得更多,那么很容易伤害自己。如果您实际上了解更多,那么只有在知道还不成熟时才进行优化。
2015年

27

通常,垃圾收集器会在遇到“内存压力”时进行收集,因此最好不要在其他时间收集垃圾,因为这可能会导致性能问题,甚至在程序执行中出现明显的暂停。而且实际上,第一点取决于第二点:对于分代垃圾收集器而言,至少,垃圾与良好对象的比率越高它的运行效率就越高,因此,为了最大程度地减少暂停程序所花费的时间,它必须拖延时间,并尽可能多地堆积垃圾。

手动调用垃圾收集器的适当时间是完成以下操作:1)可能会创建大量垃圾,并且2)用户期望花费一些时间并使系统无响应无论如何。一个典型的例子是在最后加载大的东西(文档,模型,新关等)。


12

没有人提到的一件事是,尽管Windows GC非常出色,但Xbox上的GC却是垃圾(双关语)

因此,在编写要在XBox上运行的XNA游戏时,将时间垃圾收集适时地进行是绝对至关重要的,否则您将遇到可怕的间歇性FPS打h。此外,在XBox上通常使用structs way(比平时更频繁地使用),以最大程度地减少需要进行垃圾收集的对象的数量。


4

垃圾收集首先是内存管理工具。这样,当内存不足时,垃圾收集器将进行收集。

现代垃圾收集器非常好,而且会不断完善,因此不太可能通过手动收集来改进它们。即使您今天可以改进,也很可能将来对所选垃圾收集器的改进将使您的优化无效,甚至适得其反。

但是,垃圾收集器通常不会尝试优化内存以外的资源的使用。在垃圾收集环境中,大多数有价值的非内存资源都具有close方法或类似方法,但是在某些情况下由于某些原因(例如与现有API的兼容性)并非如此。

在这些情况下,当您知道正在使用有价值的非内存资源时,手动调用垃圾回收可能是有意义的。

RMI

一个具体的例子是Java的Remote Method Invocation。RMI是一个远程过程调用库。通常,您有一台服务器,该服务器使客户端可以使用各种对象。如果服务器知道某个对象未被任何客户端使用,则该对象可以进行垃圾回收。

但是,服务器知道这一点的唯一方法是,如果客户端告诉了它,并且客户端仅告诉服务器,一旦客户端对正在使用的对象进行垃圾回收,它就不再需要对象。

这带来了一个问题,因为客户端可能有很多可用内存,因此可能不会非常频繁地运行垃圾回收。同时,服务器可能在内存中有许多未使用的对象,由于不知道客户端没有使用它们而无法收集这些对象。

RMI中的解决方案是让客户端定期运行垃圾回收,即使它有很多可用内存,也可以确保在服务器上及时收集对象。


“在这些情况下,当您知道正在使用有价值的非内存资源时,手动调用垃圾回收可能是有意义的” –如果正在使用非内存资源,则应使用using块或以其他方式调用Close方法确保尽快丢弃资源。依靠GC清理非内存资源是不可靠的,并且会引起各种问题(尤其是需要锁定才能访问的文件,因此只能打开一次)。
Jules

并且如答案中所述,当一种close方法可用时(或该资源可以与一个using块一起使用),这些才是正确的方法。答案专门针对这些机制不可用的罕见情况。
James_pic

我个人认为,管理非内存资源但提供关闭方法的任何接口都是不应使用的接口,因为无法可靠地使用它。
朱尔斯

@Jules我同意,但有时这是不可避免的。有时抽象泄漏,使用泄漏抽象比不使用抽象更好。有时,您需要使用遗留代码,这些遗留代码要求您做出自己无法保证的承诺。是的,这种情况很少见,应尽可能避免,这是有原因的,因为所有这些警告都在强制执行垃圾收集,但是确实出现了这些情况,OP正在询问这些情况是什么样的-我已经回答了。
James_pic '18

2

最佳做法是在大多数情况下不要强制垃圾回收。 (我研究过的每个系统都强制执行垃圾收集,着重强调了以下问题:如果解决了这些问题,将消除了强制执行垃圾收集的需求,并大大提高了系统速度。)

在某些情况下比垃圾收集器更了解内存使用情况。在多用户应用程序或一次响应一个以上请求的服务中,这不太可能是正确的。

但是,在某些批处理中,您对GC的了解更多。例如考虑一个应用程序。

  • 在命令行上获得文件名列表
  • 处理单个文件,然后将结果写到结果文件中。
  • 在处理文件时,会创建很多互连的对象,这些对象在文件处理完成之前无法收集(例如,解析树)
  • 在已处理的文件之间不保持匹配状态

可能可以进行案例分析(经过仔细的测试),在处理完每个文件后应强制执行完整的垃圾回收。

另一种情况是一种服务,该服务每隔几分钟醒来以处理某些项目,并且在睡眠时不保持任何状态。然后在入睡前强制进行一次完整的收集可能是值得的。

我唯一考虑强制收集的时间是当我知道最近创建了很多对象并且当前仅引用了很少的对象时。

当我可以向我提供有关此类事情的提示而不必强行使用GC时,我宁愿拥有一个垃圾收集API。

另请参阅“ Rico Mariani的演奏花絮


2

在某些情况下,您可能想自己调用gc()。

  • [ 有人说这不好,因为它可能将物体提升到更老的空间,我同意这不是一件好事。但是,并非总是如此,总会有可以提升的对象。gc()当您要创建大量对象并使用大量内存时,在此调用之后,很可能剩下很少的对象,更不用说将其移入较旧的一代空间了。您只想清除尽可能多的空间即可进行准备。这只是常识。通过gc()手动调用,将不会对要加载到内存中的那大量对象中的一部分进行多余的参考图检查。简而言之,如果您gc()在将大量内存加载到内存之前先运行,gc() 加载过程中产生的压力至少在加载开始产生记忆压力时发生至少一次。
  • 当你完成加载集合对象,您将不太可能将更多对象加载到内存中。简而言之,您从创建阶段转到使用阶段。通过gc()根据实现进行调用,将压缩所使用的内存,这将大大提高缓存的局部性。这将大大提高性能,而性能分析是您无法获得的
  • 与第一个类似,但是从如果您这样做gc()并且内存管理实现支持的角度来看,您将为物理内存创建更好的连续性。这又使新的大对象集合更加连续和紧凑,从而提高了性能

1
有人可以指出拒绝投票的原因吗?我自己对判断答案的知识还不够了解(乍一看,这对我来说很有意义)。
欧米茄

1
我猜你对第三点持反对态度。也可能会说“这只是常识”。
immibis

2
创建对象的大集合时,GC应该足够聪明,以知道是否需要一个集合。需要压缩内存时相同。依靠GC优化相关对象的内存局部性似乎是不可能的。我认为您可以找到其他解决方案(结构,不安全等)。(我不是拒绝投票的人)。
纪尧姆

3
在我看来,您的美好时光的第一个想法只是个坏建议。最近有一次收集的可能性很高,因此您再次尝试进行收集只是将对象任意地提升给后代,这几乎总是不好的。以后的世代开始需要更长的时间,增加它们的堆大小“以清除尽可能多的空间”只会使问题更加严重。另外,如果您要增加负载的内存压力,则无论如何都可能会引发集合,这会运行得更慢,因为Gen1 / 2的增加
Jimmy Hoffa 2015年

2
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.如果您连续分配大量对象,则它们已经被压缩。如果有的话,垃圾回收可能会使它们稍微混乱一些。无论哪种方式,使用密集且不会在内存中随机跳动的数据结构都会产生更大的影响。如果您使用的是天真的每个节点一个元素的链表,那么没有多少手动的GC技巧可以弥补这一点。
2015年

2

一个真实的例子:

我有一个Web应用程序,该应用程序使用了很少更改且需要非常快速访问的大量数据(对于通过AJAX进行的每一次击键响应来说足够快)。

在这里要做的很明显的事情就是将相关的图形加载到内存中,然后从那里而不是从数据库中访问它,并在数据库更改时更新图形。

但是,如果非常庞大,那么由于未来的增长,单纯的负载将占用至少6GB的内存。(我没有确切的数字,一旦很明显我的2GB机器试图处理至少6GB的数据,我便拥有了所需的所有测量值,以知道它无法正常工作)。

幸运的是,这组数据中有大量的冰棒不可改变的对象彼此相同。一旦确定某个批次与另一个批次相同,就可以将一个引用别名化为另一个引用,从而允许收集大量数据,因此将所有内容放入不到一半的演出中。

一切都很好,但是为此仍然需要大约半分钟的时间来遍历6GB以上的对象,以达到此状态。任由GC自己应付,活动在应用程序的常规模式上的峰值(每秒释放量要少得多)太尖锐。

因此,GC.Collect()在此构建过程中定期调用意味着整个过程可以顺利进行。当然,GC.Collect()在应用程序运行的其余时间中,我没有手动调用。

这个实际案例是我们何时应该使用准则的一个很好的例子GC.Collect()

  1. 使用相对少见的情况下,有很多对象可供收集(提供了价值兆字节的值,这种图的构建在应用程序的生命周期内(每周大约一分钟)非常罕见。
  2. 当性能损失相对可忍受时,请执行此操作;这仅在应用程序启动时发生。(此规则的另一个很好的例子是在游戏中的关卡之间,或游戏中的其他点之间,玩家不会因暂停而感到不安。)
  3. 配置文件,以确保确实有改善。(非常简单;“有效”几乎总是比“无效”击败)。

在大多数情况下,我认为我应该考虑一个GC.Collect()值得讨论的案例,因为应用了第1点和第2点,第3点表明情况变得更糟,或者至少使情况变得不那么好(我很少或没有改善)倾向于不调用而不是调用,因为这种方法在应用程序的整个生命周期中更有可能证明更好。


0

我有一种用于垃圾处理的用法,这有点不合常规。

不幸的是,这种错误的做法在C#世界中非常普遍,它使用丑陋,笨拙,轻率且易于出错的习惯来实现对象处置,即IDisposable-dispose。MSDN 描述它的长度,和很多人被它发誓,遵循它宗教,花时间后的时间讨论准确地说它应该怎么做等等。

(请注意,这里我所说的丑陋不是对象处置模式本身;我所说的丑陋是特定的IDisposable.Dispose( bool disposing )成语。)

之所以发明这种习惯用语,是因为我们无法保证垃圾回收器总是会调用对象的析构函数来清理资源,因此人们在其中进行资源清理IDisposable.Dispose(),以防万一,如果忘记的话,还可以从中进行更多尝试。在析构函数中。你知道,以防万一。

但是然后您IDisposable.Dispose()可能同时要清理托管对象和非托管对象,但是IDisposable.Dispose()从析构函数内部调用托管对象时,就无法清理托管对象,因为那时垃圾回收器已经处理了托管对象,因此是否需要一个单独的Dispose()方法来接受一个bool disposing标志,以知道是否应该清除托管对象和非托管对象,或者只清除非托管对象。

对不起,但这太疯狂了。

我遵循爱因斯坦的公理,它说事情应该尽可能简单,而不是简单。显然,我们不能忽略清理资源,因此,最简单的解决方案至少必须包括这一点。下一个最简单的解决方案是始终在应该处置的准确时间处置所有事物,而不必依靠析构函数作为替代方法而使事情复杂化。

现在,严格来说,当然不可能保证没有程序员会犯忘记调用的错误IDisposable.Dispose(),但是我们可以做的是使用析构函数来捕获此错误。这确实非常简单:如果析构函数检测到disposed一次性对象的标志从未设置为,则只需执行一个日志条目即可true。因此,使用析构函数不是我们处置策略的必要组成部分,而是我们的质量保证机制。并且由于这是仅调试模式的测试,因此我们可以将整个析构函数放置在一个#if DEBUG块中,因此我们绝不会在生产环境中造成任何破坏代价。(IDisposable.Dispose( bool disposing )成语规定GC.SuppressFinalize() 为了减少完成的开销,应该精确地调用它,但是使用我的机制,可以完全避免生产环境上的开销。)

归结为永恒的硬错误与软错误的论点:IDisposable.Dispose( bool disposing )习惯用语是一种软错误方法,它表示一种尝试,允许程序员Dispose()在可能的情况下忘记调用而不会导致系统失败。硬错误方法表示程序员必须始终确保Dispose()将调用该方法。在大多数情况下,硬错误方法通常规定的惩罚是断言失败,但是对于这种特殊情况,我们将例外并减少对错误日志条目的简单发布的惩罚。

因此,为了使该机制起作用,我们的应用程序的DEBUG版本必须在退出之前执行完整的垃圾处理,以确保所有析构函数都将被调用,从而捕获IDisposable我们忘记要处置的所有对象。


Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()其实不是,尽管我不认为C#能够做到。不要公开资源;取而代之的是提供一个DSL来描述您将使用它进行的所有操作(基本上是一个monad),以及一个用于获取资源,执行操作,释放它并返回结果的函数。诀窍是使用类型系统来确保如果有人走私了对该资源的引用,则该引用不能在对run函数的另一个调用中使用。
Doval 2015年

2
问题Dispose(bool disposing)(未定义)IDisposable是用于清理对象作为字段(或负责)的托管和非托管对象,这解决了错误的问题。受管对象中没有其他可处置对象的非受管对象,那么所有Dispose()方法要么是其中的一种(如果需要,终结器进行相同的清理),或者仅要处置受管对象(没有终结器)在所有的),并且需要bool disposing将消失。
乔恩·汉纳

-1错误的建议,因为最终确定实际上是如何工作的。我完全同意您的观点,即dispose(disposing)成语是Terribad,但我之所以这样说是因为人们经常在只管理资源时使用该技术和终结器(DbConnection例如,对象是托管对象,不是粉刺或混合编组的),而您仅应永远用未管理的,粉红色的,COM编组的或不安全的代码实施finalizer。我在上面的回答中详细介绍了终结器的价格是多么昂贵,除非您的班级中有非托管资源,否则不要使用它们。
吉米·霍法

2
我几乎想给您+1,尽管只是因为您谴责许多人将其视为dispose(dispoing)成语中的核心重要内容,但事实是,这是如此普遍,是因为人们如此害怕GC之类的东西,而这些东西与(dispose应该与GC有关)应该使他们仅服用处方药,而无需进行调查。对您有好处的检查,但您错过了最大的整体(它使最终敲定者比应有的机会更多)
Jimmy Hoffa

1
@JimmyHoffa感谢您的输入。我同意终结器通常只应用于释放非托管资源,但是您是否同意在DEBUG构建中此规则不适用,并且在DEBUG构建中我们应该自由使用终结器来捕获错误?这就是我在这里建议的全部内容,因此我不明白您为何对此表示反对。参见programmers.stackexchange.com/questions/288715/...了对世界的Java方面这种做法的解释更长。
Mike Nakis

0

您能告诉我在哪种情况下强制垃圾回收实际上是一个好主意吗?我不是在询问C#的特定情况,而是询问所有具有垃圾收集器的编程语言。我知道您不能在所有语言(例如Java)上强制使用GC,但让我们假设可以。

只是从理论上讲,而忽略了某些GC实现会减慢其收集周期中的问题之类的问题,我能想到的最大的场景是强制执行垃圾收集,这是一种关键任务软件,其中逻辑泄漏比悬挂指针崩溃更可取,例如,因为崩溃在不可预见的时间可能会丧生或类似的生命。

如果您看一些使用GC语言编写的更独立的独立游戏(例如Flash游戏),它们会像疯了似的泄漏,但不会崩溃。他们可能在20分钟的游戏中就消耗了10倍的内存,因为游戏代码库的某些部分忘记了将引用设置为null或将其从列表中删除,并且帧频可能开始受到影响,但是游戏仍然可以正常工作。使用同类型的C或C ++编码编写的类似游戏可能由于相同类型的资源管理错误而导致访问悬空指针而崩溃,但不会泄漏太多。

对于游戏而言,从可以快速检测和修复的角度来看,崩溃可能是更可取的,但是对于任务关键型程序,在完全意外的时间崩溃可能会导致某人死亡。因此,我认为主要的情况是不会崩溃或其他某种形式的安全性至关重要的场景,相比之下,逻辑泄漏是相对琐碎的事情。

我认为强制使用GC不好的主要情况是,逻辑泄漏实际上比崩溃更可取。例如,对于游戏而言,崩溃并不一定会杀死任何人,并且在内部测试过程中很容易发现并修复崩溃,而即使在产品交付后,逻辑泄漏也可能不会被注意到,除非它如此严重以至于在几分钟之内就无法玩游戏。 。在某些领域,测试中发生的易于重现的崩溃有时比没有人立即注意到的泄漏更可取。

我可以想到的另一种情况是,对于一个寿命很短的程序,最好在团队中强制使用GC,就像从命令行执行某项任务然后关闭它。在这种情况下,程序的生命周期太短,以至于无法进行任何形式的逻辑泄漏。逻辑泄漏,即使是大资源,通常在运行软件后几小时或几分钟内就会出现问题,因此只打算执行3秒钟的软件就不可能出现逻辑泄漏问题,并且可能使事情变得很多如果团队只是使用GC,则编写这样的短期程序更为简单。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.