深入理解Java虚拟机之G1收集器

845 阅读6分钟

前言

G1收集器是当今收集器技术发展的最前沿成果之一,早在2004年就发表了第一篇paper,在JDK7u4中提供使用,而在JDK9中Oracle官方将G1设置为默认的垃圾收集器,以取代CMS

简介

G1收集器是一款面向服务端应用的垃圾收集器,在实现高吞吐量的同时尽可能地满足垃圾收集暂停的时间,其特点主要有:

  • 并行和并发:G1能充分利用多CPU多核环境下的硬件优势来缩短STW停顿的时间,执行GC时可通过并发的方式让用户线程继续执行
  • 分代收集:与其他收集器一样,分代的概念在G1中依然得以保留,但这里的分代理念只是逻辑上的,每个Region既可能是新生代也可能是老年代
  • 空间整合:G1从整体来看是采用了标记—整理算法,但从局部(两个Region之间)来看是采用了复制算法,相比于CMS它不会产生内存碎片
  • 可预测的停顿时间:这是G1相比于CMS的另一个大优势,G1除了追求低停顿之外还可以建立可预测停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段中消耗在垃圾收集上的时间不得超过N毫秒

G1收集器

G1收集器的内存区域划分

与G1之前的垃圾收集器对于堆内存划分的区域不同,G1对于Java堆的内存布局是将整个Java堆划分为多个大小相等的独立分区(Region),虽然还保留有新生代和老年代的概念,但它们不再是物理隔离的区域,而是一部分分区的集合。启动时可以通过参数+XX:G1HeapRegionSize指定分区的大小(1M~32M,必须是2的幂),默认情况下将堆划分为2048个分区。

每个分区又被划分为若干个卡片(Card),每个卡片大小为512Byte,所有分区的卡片会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片。

其中H表示该分区存储的是巨大对象(humongous object, H-obj),即内存大小≥分区内存一半的对象。H-obj有如下的特征:

  • H-obj直接分配到老年代,避免反复拷贝移动而导致效率下降
  • H-obj会在Global Concurrent Marking的Clean up阶段和Full GC阶段被回收
  • H-obj在分配内存之前会先检查是否超过initiating heap occupancy percentthe marking threshold(是否有预留的空间供系统正常运行),如果超过的话就启动global concurrent marking(G1的混合收集阶段),为的是提早回收防止evacuation failures和Full GC而导致的效率降低或内存溢出的问题

在串行和并行收集器中,GC通过整堆扫描来确定对象是否处于可达路径。而G1为了避免整堆扫描带来的STW时间过长,在每个分区记录了一个Remember Set(RSet),用来记录引用本分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet来确定引用本分区的对象是否存活,进而确定本分区内对象存活情况。RSet和Card的关系如下图:

可见RSet本质上是一个Hash Table,key就是引用本分区对象所在的分区起始地址,而value则是该分区内对应的卡片索引集合。当然并非所有的引用都需要记录在RSet中,为什么?无论在Young GC还是在Mixed GC阶段,G1都会扫描所有的新生代分区,因此新生代分区间的相互引用记录不需要记录到RSet中;只有当新生代分区引用了老年代分区时,该老年代分区的RSet才会记录引用信息,这样就避免了在Young GC阶段为了确定对象是否存活而扫描整个老年代分区,大大减轻了GC的工作量。

那么还有一个问题:每次的引用更新都需要同步地去维护RSet中的引用记录吗?实际上这并不是同步,当出现引用更新时会有一个写屏障(Write Barriers)来暂停引用更新步骤,首先会判断该引用是否跨区引用,若是则通过卡表(Card Table)将跨区引用的对象卡片加入到队列(dirty card queue)中,并由后台的并发优化线程(Concurrent Refinement Thread)来进行异步处理,更新RSet。

G1收集器的回收模式

Young GC

发生在年轻代的GC算法,当所有的eden region被耗尽无法申请内存时,就会触发一次young gc,执行完一次young gc之后活跃对象会被拷贝到survivor region或晋升到old region中,常用的参数有:

  • -XX:G1NewSizePercent 新生代最小值,默认5%
  • -XX:G1maxNewSizePercent 新生代最大值,默认60%

Mixed GC

当越来越多对象晋升到old region时,为了避免堆内存耗尽,虚拟机会触发mixed gc,该算法并不是一个old gc,除了回收整个young region之外还会回收部分old region,这部分的old region一般是选择回收效益最高的也就是垃圾最多的region,从而可以对垃圾回收的耗时进行控制。 mixed gc也有一个阈值参数:-XX:InitiatingHeapOccupancyPercent,当old region占整个堆内存大小百分比达到该阈值时就会触发一次mixed gc

Full GC

当对象内存分配速度大于mixed gc回收速度时导致老年代被填满,就会触发一次担保机制full gc,G1收集器的full gc算法就是单线程执行的serial old gc,会导致较长时间的STW,因此需要进行多次调优避免虚拟机多次触发full gc

G1收集器和CMS收集器的比较

总的来说,G1和CMS各有优劣,G1收集器在多CPU高堆内存的运行环境下的性能是要优于CMS的,这个界限大概在6~8GB之间,当堆内存低于该界限时CMS的性能就会反过来优于G1收集器。G1收集器的另一个缺点就是相比于其他收集器而言所占用的空间更多,可以理解为空间换时间。

总结

G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。

最后如果大家看了觉得有帮助的就点个赞支持一下吧:)


参考:

《深入理解Java虚拟机JVM高级特性与最佳实践第2版》

coderlius:详解 JVM Garbage First(G1) 垃圾收集器

美团技术团队:Java Hotspot G1 GC的一些关键技术