概述
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个主要环节:
- Young GC
- 老年代并发标记过程,Concurrent Marking
- 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线程同时对对象进行修改,发生漏标的充分必要条件:
- 应用程序线程插入一个从黑色对象到白色对象的新引用。因为黑色对象已经被标记,如果不对黑色对象重新处理,那么白色对象将被漏标,造成错误;
- 应用程序线程删除所有从灰色对象到白色对象的直接或间接引用。因为灰色对象正在标记,字段引用的对象还没有被标记,如果这个引用的白色对象被删除(引用发生变化),那么这个引用对象也有可能被漏标
要避免对象的漏标,打破上述两个条件中的任何一个即可。在进行并发标记时也对应有两种不同的实现:
- 增量更新算法关注对象引用插入,把被更新的黑色或者白色对象标记成灰色,打破第一个条件
- 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
参考