JVM 垃圾收集器之G1 收集的具体步骤

·  阅读 611

G1 GC模式

G1 有两种模式

  • G1提供了两种GC模式,Young GCMixed GC 两种都是完全Stop The World的
  • Young GC:选定所有年轻代里的Region。通过控制年轻代的Region个数,即年轻代内存大小,来控制Young GC的时间开销。
  • Mixed GC:选定所有年轻代里的Region外加根据global concurrent marking统计得出收集收益高的若干老年代Region。 在用户指定的开销目标范围内尽可能选择收益高的老年代Region。
  • Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(Full GC)来收集整个GC heap。 所以本质上,G1是不提供Full GC的。

global concurre marking

  • global concurrent marking的执行过程类似于CMS但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的并不是一次GC过程的一个必须环节。
  • global concurrent marking的执行过程分为四个阶段:
    • 初始标记( initial mark,STW) :它标记了从GC Root开始直接可达的对象。
    • 并发标记(Concurrent Marking):这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。
    • 重新标记(Remark,STw):标记那些在并发标记阶段发生变化的对象,将被回收。
    • 清理(Cleanup) :清除空Region(没有存活对象的),加入到free list
    • 第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以 说global concurrent marking是伴随Young GC而发生的。
    • 第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW

G1在运行过程中的主要模式

  • YGC(不同于CMS)。
  • 并发阶段
  • 混合模式。
  • Full GC(一般是G1出现问题时发生),会启用备用收集器——serial old GC。

Mixed GC 触发时机

  • 由一些参数控制,另外也控制着哪些老年代Region会被选入CSet(收集集合)。
  • G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。

Cset垃圾集合。

  • G1MixedGCLiveThresholdPercent: old generation region中的存活对象的占比,有在此参数之下,才会被选入CSet。
    • 就是我想要存活对象占比为80%,结果没有到达设置的80%,就说明垃圾对象较多。
  • G1MixedGCCountTarget:一次globalconcurrent marking之后,最多执行MixedGC的次数。
  • G1OldCSetRegionThresholdPercent:次Mixed GC中能被选入CSet的最多old generation region数量。

其它的参数

image.png

G1 垃圾收集概览

关于分代

  • G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域
  • G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。

Humongous区域

humongous巨大的,庞大的, 来自hugemonstrous的合成词。

  • 在G1中,还有一种特殊的区域,叫Humongous区域。如果一个对象占用的空间达到或是超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。

image.png

  • 这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

G1 Young GC

  • Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
  • Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中最终Eden空间的数据为空,GC完成工作,应用线程继续执行。
  • 如果仅仅GC新生代对象,我们如何找到所有的根对象呢?老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
    • 除了虚拟机栈和方法区中的对象是GC Roots。分代垃圾回收模型中,当回收年轻代时,老年代指向新生代的引用也可以作为GC roots根节点。
  • 在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
    • point-out 指我引用了谁。
  • 但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。
  • 于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
    • point-in 指谁引用了我。
  • 需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1中又引入了另外一个概念,卡表(Card Table) 。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。
  • 默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为'O',即标记为脏,被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index

Young GC分5个阶段

  • 阶段1:根扫描
    • 静态和本地对象被扫描
  • 阶段2:更新RSet
    • 处理dirty card队列更新RSet
  • 阶段3:处理RSet
    • 检测从年轻代指向老年代的对象
  • 阶段4:对象拷贝
    • 拷贝存活的对象到survivor或old区域
  • 阶段5:处理引用队列
    • 软引用,弱引用,虚引用处理

再谈Mixed GC

全局标记

  • 在G1 GC中,global concurrent marking,要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global current marking的执行过程分为四个步骤
    • 初始标记( initial mark,STW) :它标记了从GCRoot开始直接可达的对象。
    • 并发标记(Concurrent Marking) :这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。
    • 重新标记(Remark,STW) :标记那些在并发标记阶段发生变化的对象,将被回收。
    • 清理(Cleanup) :清除空Region(没有存活对象的),加入到free list。

提到标记就不得不提 G1 的三色标记算法了。

三色标记算法

  • 我们将对象分成三种类型:
    • 黑色:根对象,或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有(成员变量)field也被标记完了)。
    • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)。
    • 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象(对象没有被标记到)。

G1垃圾收集器详解.

  • 遍历了所有对象,为白色的的就是垃圾对象。

三色标记的漏标问题

  • 因为标记线程和用户线程是并发执行的,所有就会就标记结果异常。
  • 看起来正常标记。

image.png

  • 上一步马上要将C也要标灰的,结果引用被用户线程改了。 image.png
  • 因为引用改了,B就搜索完了,由灰色变成了黑色,因为A已经是黑色了,就表示A和A指向的引用都扫描完了,就不会继续搜索了,就导致C被遗漏了,就出现了下图所示的情况。C是白色的,就会被回收,明明不应该被回收的。 image.png

为了解决三色标记的漏洞,遂引入SATB

它解决三色标记的两个问题

  • 在并发标记的时候会产生新对象,新对象创建势必会改变引用,就会影响标记结果。
  • 用户线程会改变曾经的引用,也会影响标记结果

想想CMS是不是也是因为并发标记,导致结果精度受影响,遂STW进行Remark。

SATB详解

  • 在G1中,使用的是SATB (Snapshot-At-The-Beginning)的方式,删除的时候记录所有的对象。
  • 它有3个步骤:
    • 在开始标记的时候生成一个快照图,标记存活对象。
    • 在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)。上面的漏标样例,C是B的旧引用,变成A的引用后会变成非白的。
    • 可能存在浮动垃圾,将在下次被收集。
  • SATB是维持并发GC的一种手段G1并发的基础就是SATB。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成一个对象图。
  • 在GC收集的时候,新生代的对象也认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象。
  • 如何找到在GC过程中分配的对象(就是标记的过程中有新对象产生,怎么标记的问题)呢?每个region记录着两个top-at-mark-start( TAMS:指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。通过这种方式我们就找到了在GC过程中新分配的对象,并把这些对象认为是活的对象。
  • 解决了对象在GC过程中分配的问题,那么在GC过程中引用发生变化的问题怎么解决呢?G1给出的解决办法是通过Write Barrier。Write Barrier就是对引用字段进行赋值做了额外处理。通过Write Barrier就可以了解到哪些引用对象发生了什么样的变化。
  • mark的过程就是遍历heap标记live object的过程,采用的是三色标记算法,这三种颜色为white(表示还未访问到)、gray(访问到但是它用到的引用还没有完全扫描)、black(访问到而且其用到的引用已经完全扫描完)整个三色标记算法就是从GC roots出发遍历heap,针对可达对象先标记white为gray,然后再标记gray为black;遍历完成之后所有可达对象都是black的,所有white都是可以回收的。
  • SATB仅仅对于在marking开始阶段进行"snapshot"(marked all reachable at markstart),但是concurrent的时候并发修改可能造成对象漏标记。

G1混合式回收

年轻代和老年代GC

  • G1到现在可以知道哪些老的分区可回收垃圾最多当全局并发标记完成后,在某个时刻,就开始了Mixed GC。 这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区,就是收益高的Old Region。
  • 混合式GC也是采用的复制清理策略,当GC完成后,会重新释放空间。

G1分代算法

不适合新生代

只是G1 的分代算法不适合新生代

  • 为老年代设置分区的目的是老年代里有的分区垃圾多,有的分区垃圾少,这样在回收的时候可以专注于收集垃圾多的分区,这也是G1名称的由来。
    • 分区就是分块的意思,将不同角色分成Region。
  • 不过这个算法并不适合新生代垃圾收集,因为新生代的垃圾收集算法是复制算法,但是新生代也使用了分区机制主要是因为便于代大小的调整。

停顿(STW)预测模型

G1收集器突出表现出来的一点是通过一个停顿预测模型根据用户配置的停顿时间来选择CSet的大小,从而达到用户期待的应用程序暂停时间。通过 -XX:MaxGCPauseMillis 参数来设置。这一点有点类似于ParallelScavenge收集器。关于停顿时间的设置并不是越短越好

  • 设置的时间越短意味着每次收集的CSet越小,导致垃圾逐步积累变多,最终不得不退化成Serial GC
  • 停顿时间设置的过长,那么会导致每次都会产生长时间的停顿,影响了程序对外的响应时间

当Mixed GC赶不上对象产生的速度的时候就退化成Full GC,这一点是需要重点调优的地。

G1最佳实践

  • 就是使用VM参数调整G1 的行为。

不断调优暂停时间指标

  • 通过 -XX:MaxGCPauseMillis=x 可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置。一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。

不要设置新生代和老年代的大小

  • G1收集器在运行的时候会调整新生代和老年代的大小。通过改变代的大小来调整对象晋升的速度以及晋升年龄,从而达到我们为收集器设置的暂停时间目标。
  • 设置了新生代大小相当于放弃了G1为我们做的自动调优。我们需要做的只是设置整个堆内存的大小,剩下的交给G1自己去分配各个代的小即可。

关注Evacuation Failure

  • Evacuation Failure类似于CMS里面的晋升失败,堆空间的垃圾太多导致无法完成Region之间的拷贝,于是不得不退化成Full GC来做一次全局范围内的垃圾收集。

晋升就是从新生代移到老年代。

用代码演示一下GC日志

看看就好,跟以前的GC 日志大同小异

只是收集的步骤和阶段的名字不同罢了

  • 以下为本次VM参数
-XX:+PrintGCDetails  // 打印GC 日志 
-XX:+PrintGCDateStamps  // 打印 GC 日志 时间戳
-XX:+UseG1GC   //  使用G1 收集器
-XX:MaxGCPauseMillis=200m  //   最大暂停时间(STW) 200毫秒
-Xmx20M  //   最大堆大小
-Xms10M  //   最小堆大小
复制代码

G1 一般不设置分代大小

  • 代码
public class G1Test {
    public static void main(String[] args) {
         int size = 1024*1024;
         IntStream.range(0,11).forEach(i-> {
             byte[] bytes = new byte[5 * size];
         });
    }
}
复制代码

image.png

学习GC过程的一些疑点

各个收集之间的关系是什么

啥是Full GC ,它使用的是什么收集器?

  • Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;
  • Minor GC和Major GC是俗称,在Hotspot JVM实现的Serial GC, Parallel GC, CMS, G1 GC中大致可以对应到某个Young GC和Old GC算法组合;
  • 最重要是搞明白上述Hotspot JVM实现中几种GC算法组合到底包含了什么
    • Serial GC算法:Serial Young GC + Serial Old GC (实际上它是全局范围的Full GC);
    • Parallel GC算法:Parallel Young GC + 非并行的PS Mark-Sweep GC(就是串行的Mark-Sweep,和Serial Old 类似) 或 并行的Parallel Old GC(这俩实际上也是全局范围的Full GC),选PS MarkSweep GC 还是 Parallel Old GC 由参数UseParallelOldGC来控制;
    • CMS算法:ParNew(Young)GC + CMS(Old)GC (piggyback on ParNew的结果/老生代存活下来的object只做记录,不做compaction)+ Full GC for CMS算法(应对核心的CMS GC某些时候的不赶趟,开销很大);
    • G1 GC:Young GC + mixed GC(新生代,再加上部分老生代)+ Full GC(一般是Serial Old) for G1 GC算法(应对G1 GC算法某些时候的不赶趟,开销很大);
  • 搞清楚了上面这些组合,我们再来看看各类GC算法的触发条件。简单说,触发条件就是某GC算法对应区域满了,或是预测快满了。 比如,
    • 各种Young GC的触发原因都是eden区满了;
    • Serial Old GC/PS MarkSweep GC(串行 Mark-Sweep)/Parallel Old GC的触发则是在要执行Young GC时候预测其晋升(promote)的object的总size超过老生代剩余size;
    • CMS GC的initial marking的触发条件是老生代使用比率超过某值
    • G1 GC的initial marking的触发条件是Heap使用比率超过某值,跟CMS类似;
    • Full GC for CMS算法和Full GC for G1 GC算法的触发原因很明显,就是它们的引以为傲的独特算法不赶趟了,只能全局范围大搞一次GC了(相信我,这很慢!这很慢!这很慢!);
  • PS MarkSweep GC(串行 Mark-Sweep)/Parallel Old GC(Full GC)之前会跑一次Parallel Young GC;
    • 原因就是减轻Full GC 的负担。

只有Full GC 才会STW吗?

  • Serial GC:Full GC整个过程STW,Young GC整个过程STW。
  • Parallel GC:Full GC整个过程STW,Young GC整个过程STW。
  • CMS GC:Full GC整个过程STW,Young GC整个过程STW,Old GC只有两个小阶段STW。
  • G1 GC:Full GC整个过程STW,Young GC整个过程STW,Mixed GC由全局并发标记和对象复制组成,全局并发标记其中两个小阶段STW,其它并发。

所以所有垃圾回收器的所有垃圾回收策略都会触发STW,只是时间长短不同而已。

题外话

分类:
后端
标签:
分类:
后端
标签: