G1:垃圾优先
G1收集器是热点JVM中要实现的最新收集器。 自Java 7 Update 4以来,它一直是受支持的收集器。OracleGC团队也公开表示,他们对低暂停GC的希望是完全实现的G1。 这篇文章来自我之前的垃圾收集博客文章:
- 热点GC概述 。
- 并行垃圾收集器 。
- 并发标记扫描 。
问题:大堆意味着长暂停时间
并发标记和扫描(CMS)收集器是当前推荐的低暂停收集器,但是不幸的是,其暂停时间随使用权区域中活动对象的数量而定。 这意味着,尽管使用较小的堆获得较短的GC暂停相对容易,但是一旦开始使用10或100千兆字节的堆,时间就会开始增加。
CMS也不会“整理”其堆,因此在某个时间点您会遇到并发模式故障(CMF),从而触发完整的gc。 一旦进入了完整的gc场景,您就可以预期每千兆字节活动对象大约1秒钟的时间间隔。 使用CMS,您的100GB堆可能是等待1.5分钟的GC暂停滴答定时炸弹……
良好的GC调优可以解决此问题,但有时只会将问题推向前进。 并发模式失败和完整GC不可避免地会在足够长的时间范围内出现,除非您处在一小部分专门避免填充其使用期限的人员中。
G1堆布局
G1收集器试图通过将堆分成不同的区域来将单个集合的暂停时间与堆的整体大小分开。 每个区域的大小都是固定的,介于1MB和32MB之间,JVM的目标是总共创建大约2000个区域。
您可能还记得以前的文章,其他收集器将堆分为Eden,Survior Space和Tenured内存池。 G1保留相同类别的池,但不是将它们作为连续的内存块,而是将每个区域在逻辑上分类为这些池之一。
还有另一种类型的区域-大型区域。 这些对象用于存储大小比大多数对象大的对象,例如,很长的数组。 任何大于区域大小50%的对象都存储在巨大的区域中。 它们通过获取连续位于内存中的多个普通区域并将它们视为单个逻辑区域来工作。
记忆集
当然,如果您将不得不扫描整个堆以找出哪些对象被标记为活动对象,那么将堆分成多个区域毫无意义。 实现此目标的第一步是将区域分成称为卡的512字节段。 每张卡在卡标记表中都有一个1字节的条目。
每个区域都有一个关联的记忆集或RSet-这是已写入的卡集。 如果卡片中存储的另一个区域中的某个对象指向该区域中的某个对象,则该卡片在记忆集中。
每当更改器写入对象引用时,就会使用写屏障来更新所记住的集合。 在幕后,记住的集合被分为不同的集合,以便不同的线程可以在没有争用的情况下运行,但是从概念上讲,所有集合都是同一集合的一部分。
并发标记
为了标识哪些堆对象是活动的,G1执行活动对象的大部分并发标记。
- 标记阶段标记阶段的目标是弄清楚堆中哪些对象是活动的。 为了存储哪些对象处于活动状态,G1使用了标记位图-堆中每64位存储一个位。 从所有对象的根部开始跟踪,并在标记位图中使用活动对象标记区域。 这通常是并发的,但是有一个类似于CMS的“ 初始标记暂停” ,其中应用程序被暂停并且跟踪根对象的第一级子级。 完成此步骤后,重新启动线程。 G1需要保持对堆中生活内容的最新了解,因为不会在标记阶段的同一暂停中清理堆。
- 标记阶段标记阶段的目标是使标记阶段中有关活动对象的信息保持最新。 首先要做的是确定何时进行标记。 由一定百分比的堆已满触发。 这是通过从标记阶段和此后的分配数量中获取信息来计算的,并告诉G1其是否超过了要求的百分比。 G1使用前面提到的写屏障来记录对堆的更改,并将其存储在一系列更改缓冲区中 。 更改缓冲区中的对象同时在标记位图中标记。 当达到填充百分比时,将再次暂停更改程序线程并处理更改缓冲区,从而将更改缓冲区中的对象标记为活动状态。
- 清理阶段此时,G1知道哪些对象处于活动状态。 由于G1专注于可用空间最大的区域,因此下一步是通过对活动对象进行计数来计算给定区域中的可用空间。 这是从标记位图计算得出的,并且根据最有可能收集到哪些区域来对区域进行排序。 要收集的区域存储在所谓的收集集或CSet中 。
疏散
与半球年轻一代在并行GC和CMS收集器中采用的方法类似,不会收集死物。 取而代之的是,将有生命的物体从某个区域撤离,然后将整个区域视为空闲区域。
G1对于如何回收活动物体很聪明–它不会尝试在给定的周期内回收所有活动物体。 它针对的是可能会回收尽可能多空间的区域,仅将其撤离。 它通过计算活动对象在一个区域内的比例并选择活动对象比例最低的区域来确定其目标区域。
将物体从多个其他区域撤离到自由区域。 这意味着G1在执行GC时会压缩数据。 这由多个线程并行操作。 传统的“平行GC”可以做到这一点,而CMS不这样做。
与CMS和Parallel GC相似,存在终身制的概念。 也就是说,如果年轻对象在足够的收藏中存活下来,它们就会变“旧”。 此数字称为任职期限。 如果年轻的世代区域在任职期限内幸免,并保留了足够的活物以避免被疏散,则该区域将得到提升。 首先是幸存者,最后是终身制地区。 它从未被疏散。
疏散失败
不幸的是,G1仍然会遇到类似于并发模式故障的情况,在这种情况下,它会退回到Stop the World Full GC。 这称为疏散失败,在没有空闲区域时发生。 没有自由区域意味着没有地方疏散物体。
理论上,与CMS中的并发模式故障相比,G1中发生疏散失败的可能性较小。 这是因为G1会即时压缩其区域,而不仅仅是等待压缩失败。
结论
尽管进行了压缩和在低暂停时做出的努力,但G1并不能保证一定会获胜,采用它的任何尝试都应伴随有客观且可衡量的性能目标以及GC Log分析。 所需的方法超出了本博客文章的范围,但希望我会在以后的文章中介绍它。
从算法上讲,G1会遇到其他Hotspot收集器不会遇到的开销。 值得注意的是,维护记忆集的成本。 并行GC仍然是推荐的吞吐量收集器,并且在许多情况下CMS都比G1更好。
现在说G1是否会比CMS收集器大胜还为时过早,但是在某些情况下,它已经为使用它的开发人员提供了好处。 随着时间的流逝,我们将看到G1的性能限制是否真的是G1的限制,或者开发团队是否仅需要更多的工程工作来解决那里的问题。
感谢John Oliver , Tim Monks和Martijn Verburg审阅了本期及以前的GC文章的草稿。
翻译自: https://www.javacodegeeks.com/2013/07/garbage-collection-in-java-4.html