最常见的 concurrent 收集器

大多数并发收集器同时执行部分工作到应用程序,因此他们的名字。 Java HotSpot VM包括两个主要并发的收集器:

并发标记扫描(CMS)收集器:此收集器适用于喜欢较短垃圾收集暂停且可以与垃圾收集共享处理器资源的应用程序。

垃圾优先(G1)垃圾收集器:此服务器式收集器适用于具有大量内存的多处理器计算机。它以高概率满足垃圾收集暂停时间目标,同时实现高吞吐量。

大多数并发收藏家的开销

大多数并发收集器交换处理器资源(否则可供应用程序使用)以缩短主要收集暂停时间。

最明显的开销是在集合的并发部分期间使用一个或多个处理器。在N处理器系统上,集合的并发部分使用可用处理器的K/N,其中1 <= K <= ceiling {N / 4}。

除了在并发阶段使用处理器之外,还会产生额外的开销以实现并发。因此,虽然并发收集器的垃圾收集暂停通常要短得多,但应用程序吞吐量也往往略低于其他收集器。

在具有多个处理核心的计算机上,处理器在集合的并发部分期间可用于应用程序线程,因此并发垃圾收集器线程不会暂停应用程序。这通常会导致暂停时间缩短,但应用程序可用的处理器资源也会减少,并且应该会有一些减速,特别是如果应用程序最大限度地使用所有处理内核。随着N的增加,由于并发垃圾收集导致的处理器资源的减少变得更小,并且并发收集的益处增加。请参阅并发模式故障,该故障讨论了此类扩展的潜在限制。

因为在并发阶段期间至少有一个处理器用于垃圾收集,所以并发收集器通常不会在单处理器(单核)机器上提供任何好处。

Courrent Mark Sweep(CMS)收集器

并发标记扫描(CMS)收集器专为需要较短垃圾收集暂停且能够在应用程序运行时与垃圾收集器共享处理器资源的应用程序而设计。

通常,具有相对大的长寿命数据集(大型旧代)并在具有两个或更多处理器的计算机上运行的应用程序倾向于从该收集器的使用中受益。

使用命令行选项 -XX:+UseConcMarkSweepGC 启用CMS收集器。

不推荐使用CMS收集器。

强烈考虑使用Garbage-First收集器。

并发Mark Sweep收集器性能和结构

与其他可用的收集器类似,CMS收集器是世代的;因此,小型和主要收藏都会发生。

CMS收集器尝试通过使用单独的垃圾收集器线程在执行应用程序线程的同时跟踪可访问的对象来减少由于主要收集而导致的暂停时间。

在每个主要收集周期中,CMS收集器会在集合开始时暂停所有应用程序线程,并再次暂停到集合的中间位置。第二次暂停往往是两次暂停的时间越长。多个线程在两个暂停期间执行收集工作。一个或多个垃圾收集器线程执行集合的其余部分(包括大多数活动对象的跟踪和扫描无法访问的对象)。次要集合可以与正在进行的主循环交错,并且以类似于并行收集器的方式完成(特别是,在次要集合期间应用程序线程被停止)。

并发模式失败

CMS收集器使用一个或多个与应用程序线程同时运行的垃圾收集器线程,目的是在它们变满之前完成旧一代的集合。

如前所述,在正常操作中,CMS收集器在应用程序线程仍在运行时执行大部分跟踪和扫描工作,因此应用程序线程只能看到短暂的暂停。但是,如果CMS收集器无法在旧一代填满之前完成回收不可到达的对象,或者如果无法使用旧一代中的可用空闲块满足分配,则应用程序将暂停并且收集完成所有应用程序线程都停止了无法同时完成收集称为并发模式失败,表示需要调整CMS收集器参数。

如果并发收集被显式垃圾收集(System.gc())中断或者为诊断工具提供信息所需的垃圾收集,则会报告并发模式中断。

过多的GC时间和OutOfMemoryError

如果在垃圾收集中花费了太多时间,CMS收集器将抛出OutOfMemoryError:如果在垃圾收集中花费了超过98%的总时间并且恢复了少于2%的堆,则抛出OutOfMemoryError。

此功能旨在防止应用程序长时间运行,同时由于堆太小而很少或没有进度。

如有必要,可以通过在命令行中添加选项 -XX:-UseGCOverheadLimit 来禁用此功能。

该策略与并行收集器中的策略相同,只是执行并发收集所花费的时间不计入98%的时间限制。

换句话说,只有在应用程序停止时执行的集合才会计入过多的GC时间。

此类集合通常是由于并发模式失败或显式收集请求(例如,对System.gc() 的调用)。

并发标记扫描收集器和浮动垃圾

与Java HotSpot VM中的所有其他收集器一样,CMS收集器是一个跟踪收集器,至少可以识别堆中的所有可访问对象。

Richard Jones和Rafael D. Lins在他们的出版物Garbage Collection:Algorithms for Automated Dynamic Memory中,它是一个增量更新收集器。

由于应用程序线程和垃圾收集器线程在主要集合期间并发运行,因此垃圾收集器线程跟踪的对象随后可能在时间收集过程结束时变得不可访问。这些尚未回收的无法到达的物体被称为浮动垃圾。浮动垃圾的数量取决于并发收集周期的持续时间以及应用程序的参考更新频率(也称为突变)。此外,由于年轻一代和老一代是独立收集的,每一代都是另一方的根源。作为一个粗略的指导方针,尝试将老一代的规模增加20%来解释浮动垃圾。在下一个收集周期期间收集在一个并发收集周期结束时堆中的浮动垃圾。

并发Mark Sweep Collector暂停

CMS收集器在并发收集周期中暂停两次应用程序。第一个暂停是将根目录中可直接访问的对象(例如,来自应用程序线程堆栈和寄存器的对象引用,静态对象等)以及堆中的其他位置(例如,年轻代)标记为实时。

第一次暂停被称为初始标记暂停。第二次暂停在并发跟踪阶段结束时发现,并且在CMS收集器完成对该对象的跟踪之后,由于对象中引用的应用程序线程的更新而发现并发跟踪遗漏的对象。第二次暂停被称为备注暂停。

并发标记扫描收集器并发阶段

可达对象图的并发跟踪发生在初始标记暂停和备注暂停之间。

在此并发跟踪阶段期间,一个或多个并发垃圾收集器线程可能正在使用否则可供应用程序使用的处理器资源。因此,即使应用程序线程未暂停,计算绑定应用程序也可能在此和其他并发阶段期间看到应用程序吞吐量的相应减少。备注暂停后,并发扫描阶段会收集标识为无法访问的对象。收集周期完成后,CMS收集器会等待,几乎不消耗任何计算资源,直到下一个主要收集周期开始。

启动并发收集周期

使用串行收集器时,只要旧一代变满并且在收集完成时停止所有应用程序线程,就会发生主要收集。相反,CMS收集器中并发集合的开始必须定时,以便集合可以在旧代生成之前完成;否则,由于并发模式故障,应用程序将观察到更长的暂停。有几种方法可以启动并发收集。

根据最近的历史记录,CMS收集器维护对旧一代用尽之前剩余时间的估计以及并发收集周期所需的时间。使用这些动态估计,开始并发收集周期,目的是在旧一代用尽之前完成收集周期。这些估计值是为了安全而填充的,因为并发模式故障可能非常昂贵。

如果老一代的占用率超过初始占用率(旧一代的百分比),则也开始并发收集。此初始占用阈值的默认值约为92%,但该值可能会随发行版的不同而有所变化。可以使用命令行选项-XX:CMSInitiatingOccupancyFraction = <N>手动调整此值,其中是旧生成大小的整数百分比(0到100)。

暂停计划

年轻一代收藏品和老一代收藏品的停顿独立发生。

它们不重叠,但可以快速连续发生,使得来自一个集合的暂停,紧接着来自另一个集合的一个集合,可以看起来是单个较长的暂停。 为避免这种情况,CMS收集器会尝试在上一次和下一次年轻代暂停之间大致中途安排备注暂停。 目前没有对初始标记暂停进行此调度,这通常比备注暂停短得多。

并发标记扫描收集器测量

以下是CMS收集器的输出,其中包含 -Xlog:gc 选项:

[121,834s][info][gc] GC(657) Pause Initial Mark 191M->191M(485M) (121,831s, 121,834s) 3,433ms
[121,835s][info][gc] GC(657) Concurrent Mark (121,835s)
[121,889s][info][gc] GC(657) Concurrent Mark (121,835s, 121,889s) 54,330ms
[121,889s][info][gc] GC(657) Concurrent Preclean (121,889s)
[121,892s][info][gc] GC(657) Concurrent Preclean (121,889s, 121,892s) 2,781ms
[121,892s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s)
[121,949s][info][gc] GC(658) Pause Young (Allocation Failure) 324M->199M(485M) (121,929s, 121,949s) 19,705ms
[122,068s][info][gc] GC(659) Pause Young (Allocation Failure) 333M->200M(485M) (122,043s, 122,068s) 24,892ms
[122,075s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s, 122,075s) 182,989ms
[122,087s][info][gc] GC(657) Pause Remark 209M->209M(485M) (122,076s, 122,087s) 11,373ms
[122,087s][info][gc] GC(657) Concurrent Sweep (122,087s)
[122,193s][info][gc] GC(660) Pause Young (Allocation Failure) 301M->165M(485M) (122,181s, 122,193s) 12,151ms
[122,254s][info][gc] GC(657) Concurrent Sweep (122,087s, 122,254s) 166,758ms
[122,254s][info][gc] GC(657) Concurrent Reset (122,254s)
[122,255s][info][gc] GC(657) Concurrent Reset (122,254s, 122,255s) 0,952ms
[122,297s][info][gc] GC(661) Pause Young (Allocation Failure) 259M->128M(485M) (122,291s, 122,297s) 5,797ms

注意:

CMS集合(GC ID 657)的输出中散布着次要集合的输出(GC ID 658,659和660);通常,许多次要集合在并发收集周期中发生。暂停初始标记表示并发收集周期的开始。以“Concurrent”开头的行表示并发阶段的开始和结束。暂停备注是最后的暂停。之前未讨论的是预清洁阶段。预清理代表可以同时完成的工作,为备注阶段做准备。最后阶段由并发重置指示,并为下一个并发集合做准备。

初始标记暂停通常相对于次要收集暂停时间较短。并发阶段(并发标记,并发预清除和并发扫描)通常持续时间明显长于次要集合暂停,如CMS收集器输出示例中所示。但请注意,在这些并发阶段期间不会暂停应用程序。注释暂停的长度通常与次要集合相当。备注暂停受某些应用程序特征的影响(例如,高速率的对象修改可以增加此暂停)以及自上次次要集合以来的时间(例如,年轻代中的更多对象可能会增加此暂停)。

参考资料

https://docs.oracle.com/javase/9/gctuning/concurrent-mark-sweep-cms-collector.htm