NightPxy 个人技术博客

JVM G1GC

Posted on By NightPxy

概述

G1是一种工作在分区(Region)上的并发回收器.

G1分区是一种分代算法的再进化版本.它进化,或者说抛弃了传统的代,转而以更加细化的分区为粒度,这个分区分区可以是新生代也可以是老年代,并且不需要保持连续

G1首先收集垃圾最多的分区(这是它名字的由来,Garbage First)
以分区为粒度,专注于垃圾最多的分区,再以分区为单位组织回收(新生代除外)

G1的核心思路是以频次换停顿.通过不断对老年代的部分分区执行GC来控制停顿时间

  • 因为提前分区,所以对部分分区的回收不会STW.
  • 因为每次只是部分分区,所以单次GC的速度也非常快.并且G1通过控制单次回收的分区数来进一步控制停顿时间
  • G1对年轻代始终保持STW,所以在G1中,务必要严格控制年轻代大小(最好是不控制让G1自行优化)

G1的收集获得主要包括4个

  • 新生代垃圾回收
  • 后台收集,并发周期
  • 混合垃圾回收
  • 必要的FullGC(G1一样可能产生FullGC,后面详述)

对象分配策略与大对象

对象分配主要有以下3个阶段

  • TLAB线程本地分配缓冲区
    这是一个线程私有的分配缓冲.优先从此分配.原因很简单,Eden分配必然是需要同步机制的,而提前开辟一个私有缓冲区,可以在这个缓冲区内实现无锁分配.
  • Eden分配
    TLAB分配不下时,就会尝试从Eden分配(如果是大对象就会走H区)
  • Humongous分区

**Humongous区 **
G1中有一种特殊区域叫做Humongus区(大对象区),用以专门处理大对象分配
大对象的特征是如果一个对象超过一个分区的50%,就会被认为是一个大对象.G1为此专门设计了大对象区(H区)来处理(其它是扔老年代)
如果对象大到一个区放不下,G1会寻找连续的H区存放.为了找到一个连续的H区能存放这个超大对象,G1会为此启动FullGC

G1的两张GC模式

新生代GC(YoungGC)

新生代GC主要针对Eden区,它会在Eden耗尽时触发
这种情况下

  • Eden空间移动到Survivor空间,如果Survivor不够时Eden部分对象晋升老年代
  • Survivor内移动到新的Survivor分区(类似碎片清理),部分Survivor对象晋升老年代
  • 最终Eden清空,Survivor保持紧凑,部分对象晋升老年代

新生代GC不遵循分区回收算法.而是在新生代GC时,对所有的新生代(Eden,Survivor)分区全部执行回收,之后对所有新生代分区要么回收要么晋升.即不针对具体分区,对所有年轻代分区同时起作用

混合GC(MixGC)

混合GC将同时进行正常的新生代GC和后台线程扫描标记的老年代分区

混合式GC的步骤总共两步

  • 全局并发标记(global concurrent marking)
  • 拷贝存活对象(evacuation)

全局并发标记

在进行混合GC,会先行全局并发标记,共计STW三次,步骤如下

  • 初始标记(initial mark,STW_01)
    第一步标记所有的根
  • 根区域扫描(root region scan)
    以根为起点,扫描到所有的Region,并定位到Region中连根对象作为分区的根
  • 并发标记(Concurrent Marking)
    以分区根为起点,并发扫描可达对象
    著名的三色标记法:
    黑色(根对象)->灰色(本身已扫描,但未扫描子对象.递归后有引用的会变黑)->白色(未扫描对象,垃圾)
  • 最终标记(Remark,STW_02)
    根据并发标记的结果,反向标记没有引用的对象.最终标记出应该回收的部分
  • 清理(Cleanup,STW_03)
    回收垃圾对象,清理空白区域.这个清理过程也遵循分区算法

游离对象

G1是一种与CMS类似,并发标记不STW.所以必然有一些在并发标记时发生改变的游离对象
对游离的对象的处理
CMS是增量更新,对发生变化的对象置灰,等下次重新判断
G1是STAB,快照缓存的思路.对发生变化的对象标记后下次重新判断

G1会产生FullGC的总结

G1在某些情况下,会产生FullGC.此时会退化成串行回收.STW时间将会以秒计,当然这是我们要尽量避免的.G1会产生FullGC的情况如下 :

标记失败

这是指,G1刚刚开始GC的标记阶段,MixGC还没来得及启动之前老年代就被填满了,此时会直接放弃标记过程并立即启动FullGC

此时的GC日志会出现诸如

[Full GC ....].........
[GC concurrent-mark-abort]

解决办法(排除程序本身问题情况下)

  • 增加堆大小
    在来不及启动GC的情况下就被爆了,最有可能是业务本身太重堆太小撑不住,增加堆大小
  • 调整G1后台处理周期,让它更早的启动
    -XX:ConcGCThreads,增加GC处理线程数.

晋升或疏散失败

老年代在释放内存之前就被耗尽,导致没有足够的内存以供晋升(注意这跟OOM的耗尽完全不同)
Eden疏散到survivor时也会因为相同问题导致疏散失败

反映的情况是MixGC之后随即紧跟FullGC
比如GC日志出现 to-space exhausted 或者 to-space overflow 马上FullGC

[GC pause mixed]
.....
(to-space exhausted)
[Full GC]
.....

解决办法

  • 为晋升或疏散预留更多的内存空间
    -XX:G1ReservePercent 默认10
  • 提前启动标记
    XX:InitiatingHeapOccupancyPercent 默认45%时启动标记周期

大对象分配失败

G1对大对象分配会寻找一个或多个连续H区分区,找不到合适空间就会触发FullGC

280.008: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 62344134656 bytes, allocation request: 46137368 bytes, threshold: 42520176225 bytes (45.00 %), source: concurrent humongous allocation]

解决办法

  • 不分配大对象
  • XX:G1HeapRegionSize调大单个分区大小.默认按照堆大小除以2048,注意必须是2的幂,即必须是1M,2M,4M,16M,32M这样.最小1M,最大32M
  • 一般出问题都是找不到连续分区而不是找不到分区,调大分区可以避免这种情况

G1调优

G1的调优主要体现在

  • 尽可能避免FullGC
    比如调整堆大小,新生代老年代的比率,增加GC线程让回收更加迅速和高频等
  • 让停顿时间最小化

常见的参数调优如下

-XX:MaxGCPauseMillis=N

标志一个停顿期望,默认200毫秒,默认200毫秒
如果一次停顿超过这个值G1就会开始调整尽可能弥补,比如调整老新比率,更早启动GC线程,改变晋升阈值,或是在一次GC中处理更少的分区等
通常的取舍就在这里,如果停顿的越短,新生代就会越小,但停顿的次数就会越多.一次处理的分区越少越快,但相应的积累也会更多更容易触发FullGC

-XX:G1HeapRegionSize=N

调整G1分区大小.必须是2的幂,范围是 1 MB 到 32 MB
默认是根据最小的 Java 堆大小划分出约 2048 个区域

-XX:ParallelGCThreads=N

G1垃圾回收收集的后台线程数.一般设置与Core相同,最大8
如果Core不止8个,一般为核的5/8,大数据量Spark之类的应用,可以是其逻辑处理的5/16左右(即Spark之类的不用太多,因为Spark作业本身的计算是相当繁重的)

-XX:InitiatingHeapOccupancyPercent=N

G1垃圾收集运行频率.默认45.触发GC标记周期的堆占用率阈值.45表示堆使用超过45%即触发GC
设置太高可能会导致在并发阶段来不及完成GC就导致堆占满而频繁触发FullGC
设置太低又可能会导致GC触发非常频繁,可能刚刚用了一点还不用GC时就触发GC

XX:G1ReservePercent=N

默认10,作为预留空间内存百分比

XX:G1HeapWastePercent=N

默认10,愿意浪费的百分比. 如果Region可回收比小于该阈值,则该Region不启动GC

XX:MaxMetaSpaceSize

G1的元空间上限(以前的永生代),使用堆外空间. 这个建议设置一个值,因为默认没有上限

注意

G1中最好不使用-Xmn或-XX:NewRadio等显式固定年轻代大小.
固定年轻代大小将会覆盖G1默认的暂停时间期望处理