一篇不错的关于G1的文章搬运
原文地址 https://zhuanlan.zhihu.com/p/52841787?from_voters_page=true
G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution.
One difference is that G1 is a compacting collector.
G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions.
This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues.
Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.
G1比CMS好的地方有两点
1 使用region,压缩的效率更高。能够极大地简化收集器的工作,减少碎片
2 垃圾收集暂停模型更有效率,能够解决用户指定的暂停时间
GC时G1的运行方式与CMS方式类似,会有一个全局并发标记(concurrent global marking phase)的过程,去确定堆里对象的的存活情况。并发标记完成之后,G1知道哪些regions空闲空间多(可回收对象多),优先回收这些空的regions,释放出大量的空闲空间。这是为什么这种垃圾回收方式叫G1的原因(Garbage-First)。
G1将其收集和压缩活动集中在堆中可能充满可回收对象(即垃圾)的区域,使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数量。
需要注意的是,G1不是实时收集器。它能够以较高的概率满足设定的暂停时间目标,但不是绝对确定的。根据以前收集的数据,G1估算出在用户指定的目标时间内可以收集多少个区域。因此,收集器对于收集区域的成本有一个相当准确的模型,它使用这个模型来确定在暂停时间目标内收集哪些区域和收集多少区域。
It is important to note that G1 is not a real-time collector.
It meets the set pause time target with high probability but not absolute certainty.
Based on data from previous collections, G1 does an estimate of how many regions can be collected within the user specified target time.
Thus, the collector has a reasonably accurate model of the cost of collecting the regions,
and it uses this model to determine which and how many regions to collect while staying within the pause time target.
我对于上面的英文深表赞同,最近项目上就碰到一个例子。一个32G的JVM,内存已经用了20G多还没看到gc信息。按照官方的文档,整个region占比超过45%,就会触发一次并发标记的。
但是,由于同事在上面配了暂停时间100ms,可能是100ms过短,导致G1迟迟没有进行回收,后来改为500ms后,日常平均占用内存7G。
G1中的GC收集
G1保留了YGC并加上了一种全新的MIXGC用于收集老年代。G1中没有Full GC,G1中的Full GC是采用serial old Full GC。
YGC
当Eden空间被占满之后,就会触发YGC。在G1中YGC依然采用复制存活对象到survivor空间的方式,当对象的存活年龄满足晋升条件时,把对象提升到old generation regions(老年代)。
G1控制YGC开销的手段是动态改变young region的个数,YGC的过程中依然会STW(stop the world 应用停顿),并采用多线程并发复制对象,减少GC停顿时间。
YGC是否需要扫描整个老年代?
我们知道判断对象是否存活需要从GC ROOTS结点出发,从GC ROOTS结点可达的对象就是存活的。在YGC时,老年代中的对象是不回收的,也就意味着GC ROOTS里面应包含了老年代中的对象。但扫描整个老年代会很耗费时间,势必影响整个GC的性能!。所以在CMS中使用了Card Table的结构,里面记录了老年代对象到新生代引用。Card Table的结构是一个连续的byte[]数组,扫描Card Table的时间比扫描整个老年代的代价要小很多!G1也参照了这个思路,不过采用了一种新的数据结构 Remembered Set 简称Rset。RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。每个Region都有一个对应的Rset。
Value是一个集合正好能解答我的疑惑,因为我去看了源代码,由于水平有限外加没有C++环境,只能看个文本代码。RSet里调用record方法,但是record内部实现逻辑我没找到
_sparse_table.add_card(from_hrm_ind, card_index) add_card 实在是找不到了
RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。
而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。
会扫描整个young region嘛?不过old的RSET确实不记录young到自己的引用。不过结合下面的内容来看,如果old区对象可达说明或者有别的old区引用它,或者有survivor区的对象引用它。
这里我提出我的想法,因为MIXED GC一定是伴随着YGC的,而YGC后一定会有survivor,而survivor又是G1全局并发标记的根扫描开始阶段,因此如果survivor有指向old的引用,那么old的引用一定
会被扫描到的。可能因为这个不必记录从young到old的RSET
我还有一个疑问,就是young gc会把所有的young region都回收吗?没有读到文档说会,不过我刚好找到一张young gc的日志分析文章,里面的一张图很能说明问题
注意 Edne 从1097MB到了0B,说明年轻代是全部回收了的。
也许只要别把暂停时间设得太小,就会回收全部的young region。
而且英文文档里有说,当并发标记周期开始后,如果有新的ygc,该并发周期就会中断,这就说明全局并发标记周期其实是依靠ygc的
MIXGC
G1中的MIXGC选定所有新生代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region,在用户指定的开销目标范围内尽可能选择收益高的老年代Region进行回收。所以MIXGC回收的内存区域是新生代+老年代。
在介绍MIXGC之前我们需要先了解global concurrent marking,全局并发标记。因为老年代回收要依赖该过程。
(1) Initial Mark
(Stop the World Event)This is a stop the world event. With G1, it is piggybacked on a normal young GC.
Mark survivor regions (root regions) which may have references to objects in old generation.
Initial Mark初始标记是一个STW事件,其完成工作是标记GC ROOTS 直接可达的对象。并将它们的字段压入扫描栈(marking stack)中等到后续扫描。
G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。因为 STW,所以通常YGC的时候借用YGC的STW顺便启动Initial Mark,也就是启动全局并发标记,全局并发标记与YGC在逻辑上独立。
我觉得这段翻译比较拉胯,应该是去标记survivor区(也就是有指向old区引用的区),注意这里仅仅是标记 mark,而没有进行扫描
(2) Root Region Scanning Scan survivor regions for references into the old generation. This happens while the application continues to run.
The phase must be completed before a young GC can occur.
根区域扫描是从Survior区的对象出发,标记被引用到老年代中的对象,并把它们的字段在压入扫描栈(marking stack)中等到后续扫描。
与Initial Mark不一样的是,Root Region Scanning不需要STW与应用程序是并发运行。Root Region Scanning必须在YGC开始前完成。
(3) Concurrent Marking Find live objects over the entire heap. This happens while the application is running.
This phase can be interrupted by young generation garbage collections.
不需要STW。不断从扫描栈取出引用递归扫描整个堆里的对象。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。
重复扫描过程直到扫描栈清空。过程中还会扫描SATB write barrier所记录下的引用。Concurrent Marking 可以被YGC中断,这句话的意思,在并发标记中可以允许发生ygc?
(4) Remark
Completes the marking of live object in the heap.
Uses an algorithm called snapshot-at-the-beginning (SATB) which is much faster than what was used in the CMS collector.
STW操作。在完成并发标记后,每个Java线程还会有一些剩下的SATB write barrier记录的引用尚未处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。
(5) Cleanup Performs accounting on live objects and completely free regions. (Stop the world) Scrubs the Remembered Sets. (Stop the world) Reset the empty regions and return them to the free list. (Concurrent)
STW操作,清点出有存活对象的Region和没有存活对象的Region(Empty Region)
STW操作,更新Rset。终于看到哪里更新RSET了。
Concurrent操作,把Empty Region收集起来到可分配Region队列。注意这里只是清点,并没有移动,进行region移动压缩的步骤叫 evacuation。
从上面五步可以看出来,只有跟扫描和并发标记阶段不是stw,其余阶段都是stw。
其实应该还有第六步,那就是EVACUATION,该阶段就是开始进行移动和压缩region。
经过global concurrent marking,collector就知道哪些Region有存活的对象。并将那些完全可回收的Region(没有存活对象)收集起来加入到可分配Region队列,实现对该部分内存的回收。对于有存活对象的Region,G1会根据统计模型找处收益最高、开销不超过用户指定的上限的若干Region进行对象回收。这些选中被回收的Region组成的集合就叫做collection set 简称Cset!
在MIXGC中的Cset是选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。
在YGC中的Cset是选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。
YGC与MIXGC都是采用多线程复制清除,整个过程会STW。 G1的低延迟原理在于其回收的区域变得精确并且范围变小了。