G1垃圾收集器

概述

  G1垃圾收集器(Garbage First)是一个并行的、并发的、面向服务器的垃圾收集器的垃圾收集器。G1在Oracle JDK 7 update 4 及以上版本中得到完全支持,它的长远目标时代替CMS收集器。相较于CMS,G1是一款压缩型的收集器,不会产生内存碎片;可以极高概率满足GC停顿时间,实现低停顿垃圾回收。

  G1是区域化、分代式垃圾回收器, Java对象堆被划分成若干个大小相同的区域(Region)。启动时,JVM初始化的时候决定region的大小,可以用-XX:G1HeapReginSize指定,Region的大小一定是1 MB到32 MB之间数,并且是2的幂,例如4M、16M。所有的Region都是指定的大小,在JVM生命周期内不会改变。G1的目标是产生不超过2048 个同样大小的区域,但是如果设置的Java堆大于64G,JVM会适当增加region的数量,但是region大小一定不会超过32M。G1会并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次从可回收空间最多的区域开始,尽可能回收更多的堆空间,同时尽可能不超出暂停时间目。

  在G1中没有物理上的Yong Generation(Eden/Survivor)、Old Generation,它们是逻辑的,使用一些非连续的区域(Region)组成的。而且不需要在JVM启动时决定哪些Region属于老年代,哪些属于年轻代,一个Region可能这次GC是young region,而下次GC却是old region。而且G1 GC 有一个力求达到的暂停时间目标(软实时),在年轻代回收期间,G1 GC还会调整其年轻代空间(young region个数)以满足软实时目标。

Region类型

  • Available region:可用的空闲区域。
  • Eden region: 新生代的eden区。
  • Survivor region:新生代的survivor区。
  • Humongous region: 大区。
      注意:eden region和survivor region的数量并不固定,可能伴随着GC而发生变化(young, mixed, or full GCs)。
      对G1来说,任何超过区域一半大小的对象都被视为“巨型对象(Humongous Object)”。当需要分配这种对象时,G1会找出总计内存足够包含该对象的一组连续的可用区域来分配该对象,第一个region会标记为StartsHumongous ,其它延续的region被标记为ContinuesHumongous。在分配任何巨型区域之前,会检查标记阈值,如有必要,还会启动一个并发周期。如果没有这么大的连续的可用区域,G1会作一次FGC来压缩Java堆。尽管只有一个对象,Humongous regions被认为是老年代的一部分。这样设计的目的是为了使G1在并发标记阶段,如果发现对象不可用时尽早的回收掉这些region。

CSet与RSet

  CSet即Collection Set,它是需要回收的region集合,在年轻代垃圾收集中CSet仅包含young region,在混合垃圾收集中CSet不仅包含young region,还有一些old region。

  G1中每个Region都有一个与之对应的Remembered Set,每一个RSet是一个数据结构,用以维护和跟踪Region之间的对象引用。每个region有独立的Remembered Set(RSet),减少了全堆扫描获取信息的耗时。
  虚拟机发现程序在对Reference类型的数据进行写操作时,会检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查引是否老年代中的对象引用了新生代中的对象),如果是,便把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

GC方式

G1在运行过程中主要包含如下4种GC方式

  • 年轻代垃圾收集(young collection cycle)
      当应用程序开始分配对象时,G1会在eden region分配新对象,直到eden region不够分配新对象时,young GC开始垃圾回收。在年轻代垃圾回收期间,G1 GC会同时回收eden区域和survivor区域。此阶段会有一次stop the world(STW)暂停。
      垃圾收集过程,G1将所有存活的对象从Eden Region移动到Survivor Region,即“copy to survivor”;然后晋升年轻代的存活对象到新的survivor区,对于那些年龄达到阈值(tenuring threshold)的对象会晋升到老年代。对象的晋升过程发生在负责晋升的GC线程(promoting GC thread)的本地分配缓冲区(promotion local allocation buffer,PLAB),每个GC-thread都有针对与survivor区或old区的PLAB。
      每次YGC暂停阶段G1会依据此次垃圾收集时间总时间、RSet大小、年轻代大小、暂停目标等指标计算此次年轻代垃圾收集代价,在暂停阶段结束后会基于此来重新调整年轻代的大小。

  • 并发标记(concurrent marking cycle)
      一次GC之后,当老年代的占用空间超过设置的阈值、metaspace空间超过阈值时,G1开始执行老年代的垃圾回收。通过-XX:InitiatingHeapOccupancyPercent来设置阈值,默认是45,即占用空间达到堆空间(the entire Java heap)的45%时开始并发标记阶段。
      并发标记包括以下几个阶段:

    • initial marking(初始标记阶段):在此阶段对所有的GC root进行标记,会触发一次young GC,需要stop-the-world。对应GC日志中的GC pause (young) (inital-mark)。

    • concurrent root region scanning(根区域扫描阶段):扫描和跟踪survivor区中的对象的所有的引用。该阶段是并发的,GC线程和应用线程一起执行。只有完成该阶段,才会开始下一次年轻代GC。因为下一次年轻代GC时产生的新的survivor对象,是不同于initial marking阶段后的survivor对象。

    • concurrent marking(并发标记阶段):在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。-XX:ConcGCThreads可以设置该过程的并行度,默认是ParallelGCThreads数量的四分之一。

    • remarking(重新标记阶段):该阶段会有停顿(STW),帮助完成标记周期,用来标记并发标记阶段产生新的垃圾。G1 GC清空 SATB日志缓冲区,跟踪未被访问的存活对象,并执行引用处理。该阶段是并行的,-XX:ParallelGCThreads指定并行数。

    • cleanup(清理阶段):在这个最后阶段,G1 GC 执行统计和RSet重置,会有短暂STW。在统计期间,G1 GC会识别完全空闲的区域和可供进行混合垃圾回收的区域。如果有不包含存活对象的region(即完全空闲的区域),将会有额外的concurrent-cleanup阶段,该阶段将空白区域重置并返回到空闲列表。G1会依据老年代的“GC efficiency”作排序。

  注意:如果应用程序的存活对象图非常大,那么concurrent marking cycle所耗用的时间也就越多,而且concurrent marking cycle阶段被young collection打断的次数也会变得频繁。
  marking threshold的设置非常重要,过大会导致增大发生evacuation failures的风险,过小会导致过早的触发并发标记阶段,而且可能没有垃圾需要回收。如果marking threshold的设置恰当,但并发周期的时间过长,导致混合GC阶段回收速率跟不上分配速率而触发evacuation failures,那么可以适当的增加并发线程。-XX:ConcGCThreads默认是-XX:ParallelGCThreads的四分之一,可以直接增大并发线程数量或则增大并行数。但是,需要考虑的是,增加并发线程会影响应用程序的线程,因为总的硬件资源一定。

  • 混合垃圾收集(mixed collection cycle)
      并发标记周期完成后将紧接着发生一次young collection,young collection的目的是决定是否需要触发mixed collection,如果可回收的region容量大于-XX:G1HeapWastePercent,则开始Mixed GC。
      在混合垃圾收集期间,G1 GC不仅将eden和survivor区添加到CSet,还包括并发标记阶段标记出的old区的一部分添加到CSet来作垃圾回收。所添加old区域的确切数量由一系列标志控制。G1 GC 回收了足够的old区域后(经过多次混合垃圾回收),G1 将恢复执行年轻代垃圾回收,直到下一个并发标记周期完成。

  • Full GC
      Full GC采用的类似于Serial GC一样的收集算法,当Full GC发生时,整个Java堆将会做一次压缩,以确保足够多的内存可用。但是G1中的FGC是单线程的,也会导致很长的停顿时间。当 to-space exhausted/overflow 发生时G1将采取Full GC,当然可以通过适当的参数调优,可以不触发FGC也能满足应用的性能目标。

Evacuation Failure

  对 survivors/promoted objects 进行GC时,如果JVM的heap区不足就会发生提升失败(promotion failure),如果Java堆又不能再继续扩展时将导致evacuation failure,此后就会进行一次FGC。
  当使用 -XX:+PrintGCDetails 出现evacuation failure将会在GC日志中显示:

924.897: [GC pause (G1 Evacuation Pause) (mixed) (to-space exhausted), 0.1957310 secs]
或
924.897:[GC pause (G1 Evacuation Pause) (mixed) (to-space overflow), 0.1957310 secs]

  如何避免evacuation failure,请尝试以下调整:

  • 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量,在需要更大’to-space’的情况下会尝试从该预存内存获取。
  • 通过减少 -XX:InitiatingHeapOccupancyPercent提前启动标记周期。
  • 增加-XX:ConcGCThreads选项的值来增加并发标记时的并行标记线程数目,以尽快的完成标记从而进入混合GC。

参考:http://www.oracle.com/technetwork/cn/articles/java/g1gc-1984535-zhs.html
   http://blog.csdn.net/renfufei/article/details/41897113
   http://ifeve.com/深入理解g1垃圾收集器/
   http://blog.csdn.net/lujinhong2/article/details/51130910

猜你喜欢

转载自blog.csdn.net/zero__007/article/details/83993987
今日推荐