Bootstrap

G1简介、各种GC总结

概述

G1首次出现是在JDK 6u14版本里作为体验版,JDK 7u4版本被正式推出,JDK 9中被设置为默认垃圾收集器(参考JEP 248)。

G1全称是Garbage First,目标:延迟可控的情况下,尽可能高的吞吐量。一款区域化分代式GC。

内存布局

G1是一个并行回收器,把堆内存分割为很多不相关的区域(Region,物理上可以是不连续的)。使用不同的Region来表示Eden、Survivor、Old等。
在这里插入图片描述
Survivor,幸存者区,主要作用是临时存储从年轻代的伊甸园区中存活下来的对象。G1 GC会在年轻代GC时,将伊甸园区中存活下来的对象移动到幸存者区。

仍然包括幸存者0区,幸存者1区,2个分区是交替使用的。在一次Young GC过程中,存活对象可能会从伊甸园区和S0复制到S1。在下一次GC时,S1可能成为源区,S0成为目标区,这样对象就会在这两个区域之间交替复制。这个过程也有助于追踪对象的年龄,并确定它们是否应该晋升到老年代。

G1有计划地避免在整个Java堆中进行全区域的垃圾收集,跟踪各个Region里面的垃圾堆积的价值大小,即回收所获得的空间大小以及回收所需时间的经验值,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

由于这种方式的侧重点在于回收垃圾最大量的区间,即垃圾优先(Garbage First),简称G1。

将整个Java堆划分成大约2048个大小相同且在JVM生命周期内不会被改变的独立Region,值的大小是2的幂,范围是[1MB,32MB]。

G1一个对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1新增一个内存区域,叫Humongous,专门用于存储大对象,如果一个H区装不下一个大对象,G1会寻找连续的H区来存储。为了能找到连续的H区,有时不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。

优缺点

三步:开启G1、设置堆最大内存、设置最大停顿时间

在下面的情况时,使用G1可能比CMS好:

  • 超过50%的Java堆被活动数据占用;
  • 对象分配频率或年代提升频率变化很大;
  • GC停顿时间过长,大于0.5至1秒。

缺点,引入ZGC的目的:

  • 停顿时间长:通常G1停顿时间要达到几十到几百毫秒,其实已经非常小,不能满足某些非常极端情况下的变态需求;
  • 内存利用率不高:通常引用关系的处理需要额外消耗内存,一般占整个内存的1%~20%左右;
  • 支持的内存空间有限:不适用于超大内存的系统,内存高于100GB时,会因内存过大而导致停顿时间增长;

使用建议:

  • 年轻代大小
    • 避免使用-Xmn-XX:NewRatio等相关选项显式设置年轻代大小
    • 固定年轻代的大小会覆盖暂停时间目标
  • 暂停时间目标不要太过严苛
    • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
    • 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。

特点

使用全新的分区算法,特点:

  • 并行与并发
    并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
    并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • 分代收集
    区分年轻代和老年代,年轻代有Eden区和Survivor区。但从堆的结构上看,不要求整个Eden区、年轻代或老年代都是连续的,也不再坚持固定大小和固定数量。将堆空间分为若干个区域,这些区域中包含逻辑上的年轻代和老年代和之前的各类回收器不同,同时兼顾年轻代和老年代。其他回收器工作在年轻代或老年代
  • 空间整合
    CMS:标记-清除算法、内存碎片、若干次GC后进行一次碎片整理
    G1:将内存划分为一个个Region,内存回收以Region作为基本单位的。Region之间采用复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显
  • 可预测的停顿时间模型
    即软实时soft real-time,G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    • 由于分区的原因,G1可以只选取部分区域进行内存回收,缩小回收的范围,因此对于全局停顿情况的发生也能得到较好的控制
    • G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证G1收集器在有限的时间内可以获取尽可能高的收集效率
    • 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多

模式与环节

G1提供3种垃圾收集模式:Young GC、Mixed GC和Full GC,在不同条件下触发。

G1 GC的3个主要环节:

  1. Young GC
  2. 老年代并发标记过程,Concurrent Marking
  3. Mixed GC

如果需要,单线程独占式高强度的Full GC还是继续存在,用于给GC的评估失败提供一种失败保护机制,即强力回收。

应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。

当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。

标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region即可。同时,这个老年代Region是和年轻代一起被回收的。

Young GC

Young GC的5个阶段:

  • 扫描根:根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口;
  • 更新Rset:处理dirty card queue中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用;
  • 处理Rset:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象;
  • 复制对象:对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间;
  • 处理引用:处理Soft,Weak,Phantom,Final,JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

并发标记

并发标记过程的6个阶段:

  • 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC;
  • 根区域扫描:Root Region Scanning,G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在Young GC之前完成;
  • 并发标记:Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行)此过程可能被Young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾那这个区域会被立即回收。同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例;
  • 再次标记:Remark,由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用比CMS更快的初始快照算法:Snapshot At The Beginning,SATB;
  • 独占清理:Cleanup,计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺垫,是STW的。这个阶段并不会实际上去做垃圾的收集;
  • 并发清理阶段:识别并清理完全空闲的区域。

混合回收

当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合垃圾收集器,即Mixed GC,即回收整个Young Region,还会回收一部分的Old Region。可选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。

并发标记结束以后,老年代中百分百为垃圾的内存分段被回收,部分为垃圾的内存分段被计算出来。默认情况下,这些老年代的内存分段会分8次被回收,对应参数为-XX:G1MixedGccountTarget

混合回收的回收集(collection set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。

由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。有个配置参数-XX:G1MixedGCLiveThresholdPercent,默认为65%,垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,则存活对象占比高,在复制的时候会花费更多的时间。

混合回收并不一定要进行8次。有个配置-XX:G1HeapWastePercent,默认值为10%,允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

Full GC

G1的初衷就是要避免Full GC的出现。但如果上述3个模式不能正常工作,G1会触发Full GC,发生STW,使用单线程的内存回收算法进行垃圾回收,性能差,应用程序停顿时间长。要避免Full GC的发生,一旦发生,需要进行调整。

导致G1 Full GC的原因:

  • Evacuation时没有足够的to-space来存放晋升的对象;
  • 复制存活对象时没有空的内存分段可用;
  • 并发处理过程完成之前空间耗尽;

本质还是堆内存太小,可通过增大内存解决。

参数

包括:

  • -XX:+UseG1GC:手动指定使用G1收集器执行内存回收任务;
  • -xx:G1HeapRegionSize:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000;
  • -XX:G1HeapWastePercent:默认值为10%,允许整个堆内存中有10%的空间被浪费;
  • -XX:G1MixedGCLiveThresholdPercent:默认为65%,垃圾占内存分段比例要达到65%才会被回收。若垃圾占比太低,则存活对象占比高,复制时会花费更多时间,得不偿失;
  • -XX:G1MixedGccountTarget:默认值8,表示老年代的内存分段会分8次被回收。
  • -XX:G1ReservePercent
  • -XX:G1NewSizePercent:新生代最小值,默认值5%
  • -XX:G1MaxNewSizePercent:新生代最大值,默认值60%
  • -XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms;
  • -xx:ParallelGCThreads:设置STW工作线程数的值,最多设置为8;
  • -XX:ConcGCThreads:设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右;
  • -xx:InitiatingHeapOccupancyPercent:默认值是45,触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC;
  • -XX:MaxTenuringThreshold:对象在幸存者区中经历多次GC后,如果依然存活,并达到此参数配置的年龄,可能会被晋升到老年代;

技术

String去重

背景:
大量应用分析后得出的参考数据:堆存活数据集合里String对象占25%,其中重复的String对象有13.5%,对象平均长度是45。

稍加分析,不难得知,显然是极大的内存浪费,因此引入String去重,实现原理:

  • 当垃圾收集器工作时,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象;
  • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象;
  • 使用HashTable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会检查HashTable,查看堆上是否已经存在一个一模一样的char数组;
  • 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被回收掉;
  • 如果查找失败,char数组会被插入到HashTable,以后就可共享这个数组。

三色标记

在垃圾回收中进行并发标记和并发转移这两个并发处理并不容易。在垃圾回收器标记对象或转移对象的过程中,应用线程可能正在改变对象引用关系图,从而造成漏标/错标、漏转移/错转移。

漏标不会影响程序的正确性,只是造成所谓的浮动垃圾;但错标会导致可达对象被当作垃圾回收,从而影响程序的正确性。为了区别对象的不同状态,引入三色标记法。

三色标记法是一个逻辑上的抽象,将对象用white、gray和black标记:

  • white:白色,表示没有被标记的对象,标记阶段结束后,会被当做垃圾回收掉
  • gray:灰色,表示自身已经被标记到,但其拥有的成员变量引用到别的对象还没有被标记
  • black:黑色,表示自身已经被标记到,且对象本身所有的成员变量引用到的对象也已被标记

白对象在并发标记阶段,Mutator和Garbage Collector线程同时对对象进行修改,发生漏标的充分必要条件:

  • 应用程序线程插入一个从黑色对象到白色对象的新引用。因为黑色对象已经被标记,如果不对黑色对象重新处理,那么白色对象将被漏标,造成错误;
  • 应用程序线程删除所有从灰色对象到白色对象的直接或间接引用。因为灰色对象正在标记,字段引用的对象还没有被标记,如果这个引用的白色对象被删除(引用发生变化),那么这个引用对象也有可能被漏标

要避免对象的漏标,打破上述两个条件中的任何一个即可。在进行并发标记时也对应有两种不同的实现:

  1. 增量更新算法关注对象引用插入,把被更新的黑色或者白色对象标记成灰色,打破第一个条件
  2. SATB关注的引用的删除,即在对象被赋值前,把老的被引用对象记录下来,然后根据这些对象为根重新标记一遍,用于打破第二个条件

SATB

Snapshot-At-The-Beginning,破坏第二个条件。一个对象的引用被替换时,可通过Write Barrier将旧引用记录下来。

Region中有两个top-at-mark-start指针(TAMS):prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误

SATB的副作用:如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,产生浮动垃圾。因为SATB的做法精度比较低,所以造成的浮动垃圾也会比较多。

Remembered Set

存在的问题:

  • 一个对象被不同区域引用;
  • 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用。判断对象存活时,是否需要扫描整个Java堆才能保证准确;
  • 其他分代收集器也存在这样的问题,而G1更突出;
  • 回收新生代也不得不同时扫描老年代,会降低Minor GC的效率

引入Remembered Set,简称RSet,记忆集。

解决方法:

  • 无论G1还是其他分代收集器,JVM都使用RSet来避免全局扫描;
  • 在G1里,每个Region都对应一个RSet;
  • 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
  • 检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region。其他收集器则是检查老年代对象是否引用新生代对象;
  • 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的RSet中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入RSet,就可以保证不进行全局扫描,也不会有遗漏

Pause Prediction Model

停顿预测模型

总结

汇总

垃圾收集器分类作用位置使用算法特点适用场景
Serial串行运行新生代复制算法响应速度优先适用于单CPU环境下的Client模式
ParNew并行运行新生代复制算法响应速度优先多CPU环境Server模式下与CMS配合使用
Parallel并行运行新生代复制算法吞吐量优先适用于后台运算而不需要太多交互的场景
Serial Old串行运行老年代标记-压缩算法响应速度优先适用于单CPU环境下的Client模式
Parallel Old并行运行老年代标记-压缩算法吞吐量优先适用于后台运算而不需要太多交互的场景
CMS并发运行老年代标记-清除算法响应速度优先适用于互联网或B/S业务
G1并发、并行运行新生代+老年代标记-压缩+复制算法响应速度优先面向服务端应用

示意图

在这里插入图片描述
解读:

  • 两个收集器间有连线,表明它们可以搭配使用:
    Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  • Serial Old作为CMS出现Concurrent Mode Failure失败的后备预案;
  • 红色虚线:由于维护和兼容性测试的成本,在JDK8时将Serial+CMS、ParNew+Serial Old这两个组合声明为Deprecated,参考JEP173;在JDK9中完全取消(即移除)这些组合的支持,参考JEP214;
  • 绿色虚线:JDK 14中弃用ParallelScavenge和Serial Old GC组合,参考JEP366;
  • 青色虚线:JDK14中删除CMS垃圾回收器,参考JEP363

选择

怎么选择垃圾回收器

  • 优先调整堆的大小让JVM自适应完成;
  • 如果内存小于100M,使用串行收集器;
  • 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  • 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或JVM自己选择;
  • 如果是多CPU、追求低停顿时间,需快速响应,如延迟不能超过1秒,使用并发收集器。官方推荐G1

参考

;