G1 收集器的运作过程主要步骤如下:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收: 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
从上述阶段的描述可以看出, G1收集器除了并发标记外, 其余阶段也是要完全暂停用户线程的,换言之, 它并非纯粹地追求低延迟, 官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
G1 收集器运行示意图如下:
网络异常,图片无法展示
|
G1 收集器与 CMS 收集器的异同
- 两者都非常关注停顿时间的控制
- G1 可以指定最大停顿时间、 分Region的内存布局、 按收益动态确定回收集
- 与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集
- G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间,而CMS的卡表就相当简单,只有唯一一份, 只需要处理老年代到新生代的引用, 反过来则不需要
- 它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。
通常我们说哪款收集器要更好、要好上多少,往往是针对具体场景才能做的定量比较。 按照实践经验,目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上,G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。当然,以上这些也仅是经验之谈,不同应用需要量体裁衣地实际测试才能得出最合适的结论。
垃圾收集器之间如何搭配使用?
网络异常,图片无法展示
|
如果两个收集器之间存在连线, 就说明它们可以搭配使用, 图中收集器所处的区域, 则表示它是属于新生代收集器抑或是老年代收集器。
自 JDK 9开始, ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。 官方希望它能完全被 G1 所取代, 同时,还取消了ParNew加Serial Old以及Serial加CMS这两组收集器组合的支持。至此,ParNew和CMS只能互相搭配使用。
垃圾收集器常用参数说明
- UseSerialGC:客户端模式下默认值,使用Serial+Serial old收集器组合进行回收
- UseParNewGC:JDK9之后不再支持,使用ParNew+Serial old收集器组合进行回收
- UseConcMarkSweepGC:JDK 9之前Server模式下的默认值,使用ParNew + CMS + Serial Old收集器组合进行回收,Serial Old收集器将作为CMS收集器出现“Concurrent Mode Failure“失败后的后备收集器使用
- UseParallelGC:使用Parallel Scavenge + Serial Old (PS MarkSweep)收集器组合进行回收
- UseParallelOldGC:使用Parallel Scavenge + Parallel Old收集器组合进行回收
- SurvivorRatio:指定新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Survivor=8:1
- PretenureSizeThreshold:直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
- MaxTenuringThreshold:晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代
- UseG1GC:使用G1收集器,这个是JDK 9后的Server模式默认值
- G1HeapRegionSize=n:设置Region大小,并非最终值
- MaxGCPauseMillis:设置G1收集过程目标时间,默认值是200ms,不是硬性条件
- GINewSizePercent:设置G1新生代最小值,默认值是5%
- GIMaxNewSizePercent:设置GG1新生代最大值,默认值是60%
总结
下面对这七种垃圾收集器进行了一个总结,具体如下表所示。
收集器 | 串行、并行、并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式 CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下 与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐星优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐星优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上 |
G1 | 并发 | 新生代/老年代 | 标记-整理/复制算法 | 响应速度优先 | 面向服务端应用,用来替换CMS |