【问题标题】:Why do garbage collectors freeze execution?为什么垃圾收集器会冻结执行?
【发布时间】:2012-06-19 17:27:38
【问题描述】:

我在回家的路上想着垃圾收集,我开始想,为什么垃圾收集器会完全冻结程序的执行?就我个人而言,我会设计它来阻止任何试图分配新对象的线程,但是正在运行的线程将被单独留下。 与垃圾收集器当前的工作方式相比,我无法想象在任何情况下这都会成为问题。

【问题讨论】:

  • 我想知道为什么有人反对这个?至少评论一下你为什么不投票肯定是有礼貌的吗?
  • 投反对票可能是因为这个问题提出了 GC 冻结程序的可疑断言。

标签: language-agnostic garbage-collection


【解决方案1】:

我在回家的路上想着垃圾收集,我开始想,为什么垃圾收集器会完全冻结程序的执行?

在 GC 设计中需要权衡延迟和吞吐量。您可以单独处理堆分配的块(“增量”),也可以将它们批量处理并同时处理它们(“停止世界”)。完全增量收集永远不会完全冻结程序,它的延迟非常低,但吞吐量也很差。 Stop the world 垃圾收集器的延迟可能最差(一次冻结程序几秒钟甚至几分钟),但吞吐量接近最佳。

当今所有主要的生产 GC 都提供了一个中间地带,通常是分代收集,每个线程的 Nursery 代分批收集,以及共享老年代的增量或并发收集。因此,只有 Nursery 集合会导致暂停,并且 Nursery 大小是有界的,因此暂停时间保持在较低水平,例如使用工作站 GC 在 .NET 中为 10-100 毫秒。

有关永不暂停的简单 GC 算法,请参阅Baker's Treadmill。有关垃圾收集的更多信息,我强烈推荐 Memory Management ReferenceGarbage Collection Handbook

这里的其他答案中有很多错误信息。 Jon Skeet 写了一些源代码,并开始从垃圾收集的角度来讨论它。您需要非常小心地执行此操作,因为源代码与 GC 看到的内容之间几乎没有对应关系。编译器执行指令块重新排列、寄存器分配、升级等,所有这些都会影响运行时 GC 可见的内容。特别是,源代码中的范围不会传递到已编译的代码,并且通常被liveness 的相关概念所取代。 Jon 还写道,您必须暂停才能获得全球根源。这并不完全正确,尽管它是获取全局根的最有效方式,并且由此产生的暂停几乎总是很小(亚毫秒),因为您只是从每个线程复制不到 1 kB 的堆栈。

Powerlord 写道,移动收集器必须阻塞读取,因此,所有读取的线程都会阻塞。这也是不正确的。最简单的反例是不可变数据:引用透明意味着您可以安全地读取任何副本。

Kico 写道,需要暂停来确定可达性。这也是不正确的。请参阅 Dijkstra 关于“on-the-fly”收集器和任何最近的实时 GC(例如 Stacatto)的研究。

Jerry Coffin 写了最好的答案,但移动并不是 GC 暂停的原因。有不移动但暂停的 GC(例如 HLVM)和移动但不暂停的 GC(例如 Stacatto)。

【讨论】:

    【解决方案2】:

    现代垃圾收集器(无论如何,在 .NET 和 Java 中)实际上并没有“阻止世界”——它们会做各种聪明的事情来同时收集。

    但是,您可能需要考虑这样的情况:

     object x = null;
     object y = new object();
     ...
     x = y;
     y = null;
    

    现在,假设 GC 查看 x,然后运行 ​​... 下面的行,然后 GC 查看 y - 它不会看到任何活动对象...但是对象 应该还活着。

    基本上需要一定量的暂停才能获得一致的参考集。然后是压缩、引用重新分配等。但是,就要求在整个 GC 周期中停止所有内容而言,它并没有以前那么糟糕。 确实,但是,想想会很痛苦:)

    【讨论】:

    • 这里有一篇关于 HotSpot 的 GC(有点老,诚然):ibm.com/developerworks/java/library/j-jtp11253
    • @JonHarrop:我并不是在建议源和 GC 之间直接对应。我说的是 GC 检查变量的值,然后运行一些代码,然后 GC 检查另一个变量的值。
    • @JonHarrop:我认为我们必须同意不同意。所以 cmets 并不是这个讨论的真正合适的媒介,而且我从过去与你的讨论中知道,我们必须写很多东西才能在这里取得任何实际进展。
    • @Qtax:在我所知道的任何 JVM 或 .NET 实现中都没有。首先,引用计数在周期方面存在问题。
    • @Qtax 您已经描述了在这种情况下基于范围的引用计数(例如 C++ 中的智能指针)会做什么,但大多数 GC 使用跟踪而不是引用计数,因为跟踪更快并且可以处理周期。请注意,跟踪和引用计数是相互对偶的(一个搜索生命,另一个搜索死亡),David Bacon 发表了一篇很棒的论文,名为“垃圾收集的统一理论”。 research.ibm.com/people/d/dfb/papers/Bacon04Unified.pdf
    【解决方案3】:

    除了 Kico Lobo 所说的,垃圾收集器还可以在内存中移动东西。

    因此,它们不仅要阻塞写入内存的线程,还要阻塞从内存读取的线程。

    每个线程都是这样的。

    【讨论】:

    • -1 您可以在不阻塞线程的情况下移动堆分配的块。
    【解决方案4】:

    大多数 GC 会停止执行,因为对象可以在回收周期中在内存中移动(至少在最近的设计中是这样)。这意味着在错误的时间读取或写入几乎任何对象都可能导致问题。

    有些收集器的设计理念是在给定时间仅阻塞对正在修改的内存的特定部分的读取(或写入),只要执行只使用(当前)不被修改的对象左右移动,可以畅通无阻。问题是大多数典型的硬件没有为此提供有效的支持,因此即使它们在原则上工作,但在实践中它们的效率相当低。至少有一次尝试将这种类型的算法调整为使用典型分页单元中可用的写保护,但我不知道它已被用于研究和实验之外的许多其他方面。

    主要的替代方法是使收集器增量 -- 即让它一次只做少量的工作,所以即使其他执行停止,它也只需要停止一段时间在任何给定的时间。

    但是,随着多核机器变得如此普遍,我希望看到更多的工作投入到垃圾收集算法中,这些算法可以与其他执行并行运行。直到最近,主要的重点是尽量减少花费在垃圾收集上的总时间/精力。越来越多的可用内核可能(通常)意味着在垃圾收集中做更多的总工作可能很容易证明是合理的,如果这样做可以让主流代码以更少的障碍运行。

    编辑:您可能想阅读 Paul Wilson 的 Survey of Uniprocessor Garbage Collection Techniques。这不是确定的(尤其是考虑到它的年龄),但它至少是一个合理的起点。

    【讨论】:

      【解决方案5】:

      因为这是它可以确保它要清理的引用不被其他任何人使用的唯一方法。

      如果它没有冻结执行,它无法保证。

      【讨论】:

      • 为什么?如果一个对象在收集器扫描它时持有一个引用,则持有一个引用,如果在扫描时它没有持有一个引用,则没有任何引用被持有,并且它无法取回引用。
      • 你为什么说“如果它在扫描时没有持有一个,那么就没有持有参考并且它无法获得参考”?看看 Jon Skeet 的代码 sn-p:x 一开始没有任何引用,但后来它有了。
      • 确实,Jon 在我写完评论后写了那个例子,他说得很对。
      猜你喜欢
      • 1970-01-01
      • 2015-04-06
      • 2020-03-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-12
      • 1970-01-01
      相关资源
      最近更新 更多