深入理解G1

  • Post author:
  • Post category:其他


G1是JVM历史上具有里程碑意义的收集器,开创了垃圾收集可控暂停时间的停顿时间模型。从G1开始,垃圾收集器不再追求一次将整个堆清理干净,而是追求可控的STW时间,以及在STW时间内尽可能高的内存回收速率。早期阐述Java的GC机制时,经常使用妈妈打扫房间的例子,这个例子说的是你在房间里吃瓜子,然后瓜子皮丢在地上,妈妈在打扫房间的过程中,必须在某个时刻限制你暂停吃瓜子(STW)一段时间,否则房间永远没有打扫完的时候。我当时看到这个例子时想到了两点:1、即使我被暂停吃瓜子一段时间用来打扫干净房间,可是之后我还是会继续吃瓜子并将瓜子皮丢在地上,那么这片刻的房间打扫干净是否有意义;2、妈妈不确定我会把瓜子皮丢在哪儿,那是不是意味着,房间越大,限制我暂停吃瓜子的时间就越长。

随着64位JVM的出现,以及对更大的堆内存需求增长,传统的垃圾收集器的弱点越来越明显,尤其是随着大数据基础设施的发展,大堆是刚需,即使是最成熟的ParNew+CMS,面对大数据这种对象生存周期长,突然产生大量对象的场景也是hold不住,G1的产生为可控的STW、大堆提供了可能。

一、G1的特点

停顿时间模型是G1最大的特点,停顿时间模型的意思是在GC过程中支持指定STW时间大概率不超过M毫秒。为达到此目标,G1采用了面向局部收集的设计思路和基于Region的内存布局。基于Region的堆内存布局是达成这个目标的关键,GC时将Region作为回收的最小单元,G1收集器跟踪每个Region的内存使用情况,根据历史统计值估算回收此Region能够获得的内存空间大小和暂停时间大小,然后在后台维护Region回收的优先级列表,GC时根据用户设定的暂停时间,按照优先级列表中的顺序,优先回收价值最大的Region。当然,在生产环境中,最大停顿时间的设置不能过于理想化,我们可以认为每次垃圾回收释放的内存和最大停顿时间正相关。很小的停顿时间会导致每次垃圾回收释放的堆内存很小,垃圾收集的速度跟不上分配器分配的速度,导致垃圾堆积,最终占满整个堆触发Full GC反而降低性能。

二、内存布局

G1的内存布局是将堆分为多个大小相等,彼此独立的Region,Region的大小根据启动参数取值范围为1MB~32MB,且应为2^N,每个Region都可以作为新生代Eden区,Survivor区,或者老年代空间,Region中海油一类专门放置大对象的Humongous Region。G1的内存布局如图:

三、G1的其他关键概念

Remembered Set:由于G1的面向局部收集策略,所以在回收垃圾时出现的跨Region引用对象问题,为解决此问题引入Remembered Set记录别的Region指向自己的指针。由于G1的Region数量比传统收集器的分代数量多很多,所以G1收集器要比其他传统垃圾收集器有更高的内存占用负担。

Card Table:G1引入的数据结构,是Remembered Set的一种实现,用于记录非收集区域是否存在指向收集区域的指针,类似Hash表。

Collect Set:在垃圾收集阶段,由G1垃圾回收器选择的待回收的Region集合。

写屏障:指的是改变内存值的时候额外执行一些操作。Java使用的分代垃圾回收基本都会用到写屏障,主要处理跨代引用的问题,比如将新生代对象的引用写入老年代对象进行写操作时,如果此时垃圾回收线程也在工作,那么需要仔细处理老年代对象对新生代对象的引用,避免出现引用没有被正确及时的探测到,进而存活对象被过早回收的问题。

四、G1进行GC的流程

G1工作是有两种模式,分别是Young Gc模式和Mixed GC模式,两种都是要STW的。Young GC模式和PareNew的非常像,就不详细介绍了,主要区别就是Eden区和Survivor区不再是一整块内存,而是由多个Region逻辑组成。Mixed GC回收时,不区分是新生代还是老年代,而是按照能从Region中回收的垃圾最多,回收收益大排序,组成回收集,对回收集中的Region进行回收,正常情况下新生代的Eden区都在回收范围内。如果设置的暂停时间过短,可能会导致Mixed GC回收时来不及回收Old区。Mixed GC的工作流程如下:

整个过程分为两个大的阶段:标记阶段和拷贝存活对象阶段。标记阶段的主要目标是扫描堆内存使用情况,估算每个Region的垃圾回收收益。拷贝存活对象阶段决定回收哪些Region,将Region里的存活对象复制到空Region中,再清理掉整个旧Region的全部内存空间。具体过程分为以下四个步骤:

  • 初始标记(Initial Marking):G1的这个阶段与Minor GC同步完成的,此阶段标记通过GC Roots能直接关联到的对象(这个阶段需要STW),之后将复制到新Region的Survivor区的对象扫描并标记为根;
  • 并发标记(Concurrent Marking):对全堆的对象图进行可达性分析,找出待回收对象。这个阶段运行时间比较长,但是可以与应用程序并发进行,对象图扫描完之后,还需要处理并发标记过程中有引用变动的对象;
  • 重新标记(Final Marking):仍然需要一个STW阶段,用于处理并发标记阶段结束后仍然有引用变动的对象;
  • 筛选回收(Cleanup):根据标记的可达对象,更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户设置的JVM暂停时间制定回收计划,决定回收那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。此过程设计到了存活对象的复制,需要STW。

五、G1的缺点

和CMS算法相比,G1优点很多,最主要的是从整体上是基于”标记-清除“算法实现的垃圾收集器,但从局部上,又是基于”标记-复制“算法实现,这标志着G1在运行期间不会产生内存碎片,有利于程序长时间运行。

G1相比CMS的弱点也有不少,主要表现就是G1的Remembered Set对内存有更高的需求,某些极端情况下会达到堆空间的20%甚至更多,GC进行时垃圾回收器的执行负载较高,导致应用的吞吐量不如CMS高。目前,在小内存的堆上,CMS的表现大概率上仍然优于G1;在大内存的堆上,G1则更能发挥优势,所以在小于6G~8G的堆上使用CMS可能有更好的性能,而在更大的堆上应该优先选择G1。

参考资料

1、深入理解Java虚拟机-JVM高级特性与最佳实践(第3版)

2、垃圾回收算法手册

个人公众号:

自己在软件这个行业10来年的工作和学习历程中犯了不少错误,也走了不少弯路,在此公众号分享自己的成长心路和工作心得,希望给后来的从业者一个参照,不要再犯我犯过的错误,不要再走我走过的弯路。在此我也会分享工作中遇到的技术问题和自己研究技术的记录与心得;在项目过程中遇到的风险和暴露于化解风险的过程和方法;在团队管理过程中的心得和体会。后续会发布一系列专题技术文章,程序员成长系列文章,项目的系列文章,行业发展分析和展望的文章,甚至会包含婚恋和育儿的心得文章,我是个不专注却热爱生活的工程师!



版权声明:本文为liuyh175原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。