垃圾回收和线程

23
据我所知,当垃圾收集器在执行任务时,虚拟机会阻塞所有正在运行的线程——或者至少在压缩堆时会这样。在现代CLR和JVM的实现中(截至2010年1月的生产版本),是否还是这种情况?请不要提供GC的基础链接,因为我已经理解了其基本工作原理。
我认为全局锁定是必须的,因为在压缩发生时,引用可能在移动期间无效,直接锁定整个堆似乎是最简单的方法(即通过阻止所有线程间接实现)。我可以想象更强大的机制,但是简单往往是最好的。
如果我的理解有误,请简要解释最小化阻塞的策略。如果我的假设是正确的,请提供以下两个问题的一些见解:
  1. 如果确实如此,那么重量级企业引擎(如JBOSS和Glassfish)如何保持始终高的TPS速率?我在谷歌上搜索了一下JBOSS,并期望找到适用于Web处理的类似APACHE的内存分配器的相关信息。
  2. 面对NUMA式架构(可能是不久的将来),除非进程受线程和内存分配的限制,否则这听起来像一场灾难。
6个回答

17
答案是这取决于所使用的垃圾收集算法。在某些情况下,您是正确的,在GC期间所有线程都会停止。在其他情况下,您是错误的,因为在普通线程运行时垃圾回收会继续进行。要了解GC如何实现这一点,您需要详细了解垃圾收集器的理论和术语,再加上对特定收集器的理解。它不适合简单的解释。

哦,还值得指出的是,许多现代收集器没有一个明显的压缩阶段。相反,它们通过将活动对象复制到新的“空间”并在完成后清零旧的“空间”来工作。

如果我是错误的,那么我的问题可以通过对最小化阻塞的策略进行简单的解释来回答。

如果你真的想了解垃圾收集器的工作原理,我建议你阅读以下书籍:

......请注意,要找到有关生产垃圾收集器的内部的准确,详细且公开的描述并不容易。(尽管在Hotspot GC的情况下,您可以查看源代码......)

编辑:根据OP的评论......

“看起来就像我想的那样 - 没有绕过“停止世界”的部分。” 这取决于情况。在Java 6 Concurrent Collector的情况下,在标记根(包括堆栈)期间会有两个暂停,然后标记/复制其他对象会并行进行。对于其他类型的并发收集器,在收集器运行时使用读取或写入屏障,以捕获否则会干扰彼此的收集器和应用程序线程的情况。我现在没有我的[Jones]副本,但我还记得可以使“停止世界”间隔变得微不足道...代价是更昂贵的指针操作和/或不收集所有垃圾。

(+1) 我一定会买那本书。我想更多地了解的一个问题是,在“复制到新的并更新过程”之后会发生什么?假设所有引用都通过阻止所有线程来更新?还是这是一个迭代的过程? - Hassan Syed
2
据我所知,即使是最并发的垃圾收集器,在执行某些任务时也会“停止世界”,尽管大多数任务确实是并发运行的。然而,JVM 6中的CMS收集器在停止世界阶段确实使用了所有可用的CPU。 - edgar.holleis
@edgar 谢谢,"CMS-collector" 是有用的搜索词 -- 我有时间时会查阅详细信息。看起来正如我所料 -- 避免 "停顿世界" 部分是不可能的。有些假设或一些关于这些全局锁定阶段对严肃生产服务器(如我在问题中提到的服务器)的 TPS 率产生影响的参考数字将是有趣的。 - Hassan Syed
关于TPS:你完全误解了!通常情况下,您可以将延迟(以毫秒为单位,数值越低越好)与吞吐量(以TPS为单位,数值越高越好)进行交换。低延迟算法通常比另一种选择——高吞吐量算法——具有更低的吞吐量,而后者通常也会引入更高的延迟。如果您主要担心TPS,这在服务器负载中很常见,请不要担心停止世界阶段的长度。它相对较长,但不会对您的TPS产生太大影响。 - edgar.holleis
@Hassan - @edgar 是正确的。最大化 TPS 的方法是使用总开销最低的 GC,而不是最小化暂停时间的 GC。 - Stephen C
@edgar 和 @stephen 把事情看得更清楚了。 - Hassan Syed

2
您说得对,垃圾收集器必须暂停所有应用程序线程。使用Sun JVM可以通过使用并发收集器来减少这种暂停时间,该收集器可以在不停止应用程序的情况下执行部分工作,但它仍然必须暂停应用程序线程。
有关Sun JVM如何在最新的JVM中管理垃圾回收的详细信息,请参见此处http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html#par_gc和此处http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html#cms
对于Web应用程序,我认为这不是一个问题。由于用户请求应该在很短的时间内完成,即小于1秒钟,为了服务请求分配的临时对象不应该退出年轻代(前提是其大小适当),在那里它们可以被高效清理。其他寿命周期较长的数据,例如用户会话,将会更久地存在,并可能影响主要GC事件所花费的时间。
在高TPS应用程序中,一种常见策略是在同一台或不同的硬件上使用会话亲和性和负载平衡运行多个应用程序服务器实例。通过这样做,每个JVM的单个堆大小都保持较小,从而减少进行主要收集时GC的暂停时间。通常情况下,数据库成为瓶颈而不是应用程序或JVM。
在J2EE中,最接近Web特定内存分配器概念的是由框架和应用程序服务器执行的对象/实例池。例如,在JBOSS中,您有EJB池和数据库连接池。但是,这些对象通常是因为它们的高创建成本而被池化,而不是因为垃圾回收开销。

(+1) 很好的讨论 - 我得出结论需要查看并发“小周期”收集器以了解其影响。假设 Web 服务器的 TPS 特性高度依赖于请求分配保持为年轻代对象。 - Hassan Syed
@ Hassan是的,关于年轻一代的段落是一种假设,并且将取决于您的应用程序,尽管您可以调整JVM中代的大小。重点是,您不应太担心在处理请求时创建的临时对象,因为当前的分代垃圾收集器非常擅长清理它们。运行多个JVM可以帮助处理较长寿命的对象,例如用户会话,因为它们可以分布在JVM之间,这意味着每个JVM在进行主要收集时需要检查的内容更少。 - Aaron
-1:垃圾收集器不必暂停所有线程(也称“停止世界”)。 - J D

1

我相信IBM已经进行了一些研究,以改善多核系统中的GC性能,其中包括减少或消除“全部停止”问题。

例如,请参见: 服务器的并行、增量和并发GC(pdf)

或者搜索类似于“IBM并发垃圾回收”的内容。


1
据我所知,当GC在执行其任务时,VM会阻塞所有正在运行的线程,或者至少在压缩堆时会这样。在现代实现的CLR和JVM(截至2010年1月的生产版本)中是否是这种情况?
Sun的Hotspot JVM和Microsoft的CLR都有并发GC,仅在短时间内停止全局快照的自洽快照(从其中可到达所有活动数据的全局根)而不是整个收集周期。我不确定它们的压缩实现,但这是非常罕见的。
如果确实是这种行为,那么像JBOSS和Glassfish这样的重量级企业引擎如何保持一致的高TPS速率?
这些引擎的延迟比停止全球所需的时间长几个数量级。此外,延迟被引用为例如95th百分位数,这意味着延迟只有在引用的时间跨度以下的时间内才会低于引用的时间跨度95%的时间。因此,压缩不太可能影响引用的延迟。

0

Java提供了多种垃圾回收算法,其中并非所有算法都会阻塞所有运行线程。例如,您可以使用-XX:+UseConcMarkSweepGC选项与应用程序并发运行(用于收集老年代)。


它们在需要进行全局GC时偶尔会阻塞运行线程。尽管如此,并发GC确实尝试以并发方式完成大部分工作。 http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html#cms.pauses - Paul Wagland
也称为(大多数情况下)并发标记清除。其中的“大多数情况下”是有意义的。 - Tom Hawtin - tackline
其他JVM也有完全并发的垃圾收集器。 - J D

0

目前Java的最先进垃圾回收技术仍然涉及偶尔的“停顿”现象。在Java 6u14中引入的G1 GC可以并发地完成大部分工作,但是当内存非常低且需要压缩堆时,它必须确保没有人会干扰其下面的堆。这要求不允许其他任何事情继续进行。要了解有关G1 GC的更多信息,请查看Sun的演示文稿


Java的最先进的垃圾收集器已经完全并发多年了。现在有一些实时JVM... - J D

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接