G1 垃圾收集器
G1⼜可以理解为在 CMS 垃圾收集器上进⾏升级。G1 垃圾收集器可以给你设定⼀个你希望 Stop The Word 停顿时间,G1 垃圾收集器会根据这个时间尽量满⾜你。在 G1 垃圾收集器的世界上,堆的划分不再是「物理」形式(新生代、老年代),⽽是以「逻辑」的形式进⾏划分。
不过,像之前说过的「分代」概念在 G1 垃圾收集器的世界还是⼀样奏效的,⽐如说:新对象⼀般会分配到 Eden 区、经过默认 15 次的 Minor GC 新⽣代的对象如果还存活,会移交到⽼年代等等...
在 G1 里,堆被划分成了多个同等份的区域,每个区域叫做 Region。也有老年代、新生代、Survivor 区,规则是跟 CMS 一样的。G1 中,还有⼀种叫 Humongous(⼤对象)区域,其实就是⽤来存储特别⼤的对象(⼤于 Region 内存的⼀半),⼀旦发现没有引⽤指向⼤对象,就可直接在年轻代的 Minor GC 中被回收掉。
以前的垃圾收集器都是对「堆」进行「物理」划分,如果堆空间(内存)⼤的时候,每次进⾏「垃圾回收」都需要对⼀整块⼤的区域进⾏回收,那收集的时间是不好控制的,所以 G1 划分了多个⼩区域,之后那对这些「⼩区域」回收就容易控制它的「收集时间」了。
GC 过程
Minor GC
在 G1 收集器中,可以主要分为有 Minor GC(Young GC) 和 Mixed GC,也有些特殊场景可能会发⽣Full GC。先从 Minor GC 开始,G1 的 Minor GC 其实触发时机跟前⾯提到过的垃圾收集器都是⼀样的,等到 Eden 区满了之后,会触发 Minor GC。Minor GC 同样也是会发⽣Stop The World 的。
要补充说明的是:在 G1 的世界⾥,新⽣代和⽼年代所占堆的空间是没那么固定的(会动态根据「最⼤停顿时间」进⾏调整)。这块要知道会给我们提供参数进⾏配置就好了,所以,动态地改变年轻代 Region 的个数可以「控制」Minor GC 的开销。
Minor GC 详细的回收过程可以简单分为三个步骤:根扫描、更新&&处理 RSet、复制对象。
第⼀步应该很好理解,因为这跟之前 CMS 是类似的,可以理解为初始标记的过程。
第⼆步涉及到「Rset」的概念。这里可以说到 CMS 的 Minor GC 是通过「卡表」(cart table)来避免全表扫描老年代的对象,因为 Minor GC 是回收年轻代的对象,但如果⽼年代有对象引⽤着年轻代,那这些被⽼年代引⽤的对象也不能回收掉。
同样的,在 G1 也有这种问题(毕竟是 Minor GC)。CMS 是卡表,⽽G1 解决「跨代引⽤」的问题的存储⼀般叫做 RSet,只要记住,RSet 这种存储在每个 Region 都会有,它记录着「其他 Region 引⽤了当前 Region 的对象关系」。
对于年轻代的 Region,它的 RSet 只保存了来⾃⽼年代的引⽤(因为年轻代的没必要存储啊,⾃⼰都要做 Minor GC 了),⽽对于⽼年代的 Region 来说,它的 RSet 也只会保存⽼年代对它的引⽤(在 G1 垃圾收集器,⽼年代回收之前,都会先对年轻代进⾏回收,所以没必要保存年轻代的引⽤)。RSet 概念,⽆⾮就是处理 RSet 的信息并且扫描,将⽼年代对象持有年轻代对象的相关引⽤都加⼊到 GC Roots 下,避免被回收掉。
第三步也挺好理解的:把扫描之后存活的对象往「空的 Survivor 区」或者「⽼年代」存放,其他的 Eden 区进⾏清除,这⾥要提下的是,在 G1 还有另⼀个名词,叫做 CSet。它的全称是 Collection Set,保存了⼀次 GC 中「将执⾏垃圾回收」的 Region。CSet 中的所有存活对象都
会被转移到别的可⽤Region 上,在 Minor GC 的最后,会处理下软引⽤、弱引⽤、JNI Weak 等引⽤,结束收集。
Mixed GC
当堆空间的占⽤率达到⼀定阈值后会触发 Mixed GC(默认 45%,由参数决定),Mixed GC 依赖「全局并发标记」统计后的 Region 数据。「全局并发标记」它的过程跟 CMS⾮常类型,步骤⼤概是:初始标记(STW)、并发标记、最终标记(STW)以及清理(STW)。
还是需要提前说明下:Mixed GC 它⼀定会回收年轻代,并会采集部分⽼年代的 Region 进⾏回收的,所以它是⼀个“混合”GC。⾸先是「初始标记」,这个过程是「共⽤」了 Minor GC 的 Stop The World(Mixed GC ⼀定会发⽣Minor GC),复⽤了「扫描 GC Roots」的操作。
在这个过程中,⽼年代和新⽣代都会扫,总的来说,「初始标记」这个过程还是⽐较快的,毕竟没有追溯遍历。接下来就到了「并发标记」,这个阶段不会 Stop The World,GC 线程与⽤户线程⼀起执⾏,GC 线程负责收集各个 Region 的存活对象信息,从 GC Roots 往下追溯,查找整个堆存活的对象,⽐较耗时。接下来就到「重新标记」阶段,跟 CMS⼜⼀样,标记那些在「并发标记」阶段发⽣变化的对象。CMS 在「重新标记」阶段,应该会重新扫描所有的线程栈和整个年轻代作为 root,而不是这样的。
G1 中解决「并发标记」阶段导致引⽤变更的问题,使⽤的是 SATB 算法,可以简单理解为:在 GC 开始的时候,它为存活的对象做了⼀次「快照」,在「并发阶段」时,把每⼀次发⽣引⽤关系变化时旧的引⽤值给记下来,然后在「重新标记」阶段只扫描着块「发⽣过变化」的引⽤,看有没有对象还是存活的,加⼊到「GC Roots」上。
不过 SATB 算法有个⼩的问题,就是:如果在开始时,G1 就认为它是活的,那就在此次 GC 中不会对它回收,即便可能在「并发阶段」上对象已经变为了垃圾。所以,G1 也有可能会存在「浮动垃圾」的问题,但是总的来说,对于 G1⽽⾔,问题不⼤(毕竟它不是追求⼀次把所有的垃圾都清除掉,⽽是注重 Stop The World 时间)。
最后⼀个阶段就是「清理」,这个阶段也是会 Stop The World 的,主要清点和重置标记状态,会根据「停顿预测模型」(其实就是设定的停顿时间),来决定本次 GC 回收多少 Region。⼀般来说,Mixed GC 会选定所有的年轻代 Region,部分「回收价值⾼」的⽼年代 Region(回收价值⾼其实就是垃圾多)进⾏采集,最后 Mixed GC 进⾏清除还是通过「拷⻉」的⽅式去⼲的,所以,⼀次回收未必是将所有的垃圾进⾏回收的,G1 会依据停顿时间做出选择 Region 数量。
什么时候发⽣ full GC
如果在 Mixed GC 中⽆法跟上⽤户线程分配内存的速度,导致⽼年代填满⽆法继续进⾏Mixed GC,就⼜会降级到 serial old GC 来收集整个 GC heap,不过这个场景相较于 CMS 还是很少的,毕竟 G1 没有 CMS 内存碎⽚这种问题。
总结
- 从原来的「物理」分代,变成现在的「逻辑」分代,将堆内存「逻辑」划分为多个 Region
- 使⽤CSet 来存储可回收 Region 的集合
- 使⽤RSet 来处理跨代引⽤的问题(注意:RSet 不保留 年轻代相关的引⽤关系)
- G1 可简单分为:Minor GC 和 Mixed GC 以及 Full GC
- 【Eden 区满则触发】Minor GC 回收过程可简单分为:(STW) 扫描 GC Roots、更新&&处理 Rset、复制清除
- 【整堆空间占⼀定⽐例则触发】Mixed GC 依赖「全局并发标记」,得到 CSet(可回收 Region),就进⾏「复制清除」
- R⼤描述 G1 原理的时候,从宏观的⻆度看 G1 其实就是「全局并发标记」和「拷⻉存活对象」
- 使⽤SATB 算法来处理「并发标记」阶段对象引⽤可能会修改的问题
- 提供可停顿时间参数供⽤户设置(G1 会尽量满⾜该停顿时间来调整 GC 时回收 Region 的数量)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: CMS 垃圾回收器
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论