JVM基础系列:G1垃圾回收器

  一、G1垃圾收集器

  1. G1(Garbage-First) 是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
  2. 在JDK1.7版本正式启用,是JDK9以后的默认垃圾收集器,取代了CMS收集器。

  为什么叫做Garbage First?

  1. G1 是一个并行回收器,他把堆内存分割喂很多不相关的区域(Region物理上不连续),默认把推分为2048个区域,每一个Region的大小是1-32M不等,但必须是2的整数次幂。使用不同的Region可以表示Eden、Survivor From、Survivor To、老年代等。
  2. 每次根据允许的收集时间,优先回收价值最大的Region(在后台维护一个优先列表)
  3. 由于侧重于回收垃圾最大量的区间,所以把此收集器取名为Garbage First (垃圾优先)
  4. 官方给G1设定的目标是在延迟可控的情况下,获得尽可能高的吞吐量。

  二、分区Region

  1. 使用G1收集器时,他将整个Java推划分成2048个大小相同的独立Region块,每个Region块大小根据推空间的实际大小而定的,单个Region被控制在1MB到32MB之间,并且是2的N次幂,即1、2、4、8、16、32MB。通过参数-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会改变。
  2. 虽然保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。如图
  3.  一个Region有可能属于Eden、survivor或者Old区,但是一个Region只可能属于一个角色。图中E代表Eden,S代表Survivor,O代表Old。空白表示未使用区域。

  4. G1还新增加了一种新的内存区域H,叫做Humongous区域,图中H块,主要存储大对象,如果对象超过1.5个region,就放到H。

  三、G1垃圾收集器的特点、缺点

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

  四、参数设置

  1. -XX:+UseG1GC 指定使用G1收集器(JDK9后默-认G1)
  2. -XX:G1HeapRegionSize 设置每个region大小。值是2次幂,范围是1MB到32MB,目标是根据最小的java堆划分出约2048个区域。默认是堆内存呢的1/2000
  3. -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间(jvm会尽力实现,但不保证能达到)。
  4. -XX:ParallelGCThreads 设置stw时GC线程数的值,最多设置为8.
  5. -XX:ConcGCThreads 设置并发标记线程数,将n设置为并行垃圾回收线程数(ParallelGCTheads)的1/4左右。
  6. -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的JAVA堆占用率阈值。超过此阈值触发GC。默认值是45。

  五、G1的使用场景

  • 面向服务端应用,针对具有大内存、多处理器的机器(在普通大小的堆里表现并不突出)。最主要为需要低GC延迟,并具有大堆的应用程序提供解决方案。如:在堆大小约6GB时或更大时,可预测的暂停时间可以低于0.5毫秒(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC的停顿时间不会过长);
  • 用来替换掉JDK1.5中的CMS收集器,在下面的情况时,使用G1可能比CMS好:
    1.超过50%的java堆被活动数据占据;
    2.对象分配频率或年代提升频繁变化很大;
    3.GC停顿时间过长(长于0.5至1秒);

  六、G1回收期垃圾回收过程

  1. G1 GC的垃圾回收过程主要包括如下三个环节:
    年轻代GC(Young GC)
    老年代并发标记阶段
    混合回收(Mixed GC)
    ps:如需要,单线程、独占式、高强度的Full GC还是继续存在的。他针对GC的评估失败提供一种失败保护机制,即强力回收。
    顺时针young gc ->young gc + concurrent mark ->mixed gc顺序进行垃圾回收 ;
  2.  应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行(多个垃圾线程)的独占式收集器。在年轻代回收期间,G1 暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年代区间,也有可能两个都会涉及;

  3. 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程;
  4. 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代回收,一次只需要扫描/回收一小部分老年代的Region就可以了,同时这个老年代Region是和年轻代一起被回收的。

  七、记忆集与写屏障

  1. 一个对象被不同区域引用可能导致的问题:
    一个region不可能孤立的,region中的对象可以被任意region中的对象引用,如新生代中引用了老年代,这样垃圾回收时,会去扫描老年代,会出现STW
    判断对象存活时,是否需要扫描整个Java堆才能保证准确?
    在其他分代收集器,也存在这样的问题?(而G1更突出)
    回收新生代也不得不同时扫描老年代?
    这些都会降低Young GC的效率
  2. 解决方法:
    G1和CMS一样,都是使用 Remembered Set 来避免全局扫描。在CMS中,老年代中有一块区域来记录指向新生代的引用,这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。但在G1中没有使用point-out方式,因为分区太多,这样会造成大量浪费,所以使用了ponit-in的方式来解决,分区中的Rset存储的是哪些分区引用了当前分区中的对象,这样仅仅需要扫描这些分区就可以避免很多无效扫描。
    G1在每次引用类型数据写操作时,都会产生一个Write Barrrier (写屏障),类是aop
    然后检查将要写入的引用指向的对象是否和该引用类型数据在不同的region(CMS收集器:检查老年代对象是否引用了新生代对象)
    如果不同,通过Card Table (卡表) 把相关引用信息记录到被引用对象的所在Region对应的Remembered Set 中;
    当进行垃圾回收时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏

  八、回收细节详解

  ①. G1回收过程一:年轻代GC

    回收时机

    (1).当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程

    (2).年轻代垃圾只回收Eden、survivor区,

    回收前:

 

    回收后:

 

   第一阶段,根扫描:

  考虑Remmebered Set,看是否有老年代中的对象引用了新生代

  GC Roots根对象,两栈,两方法(本地方法栈上引用的对象,虚拟机栈引用的对象、方法区类静态变量、常量引用的对象)

  第二阶段,更新RSet:

  处理dirty card queue(见PS)中的card,更新RSet。此阶段完成后,RSet可以准确的反应老年代对所在内存分段中对象的引用

   PS:  dirty card queue:对于应用程序的引用赋值语句a.field=b,JVM会在之前和之后执行特殊的操作在dirty card queue中入队一个保存了对象引用信息的card。在年轻代回收的时候,G1会对dirty card queue所有的carf进行处理,以更新RSet,保证RSet实时准确的反映引用关系。那为什么不在引用赋值语句处直接更新RSet呢?这是为了性能的需要,RSet的处理需要线程同步,开销很大,使用队列性能会好很多。

   第三阶段,处理RSet:

  识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象

  第四阶段,复制对象:

  此阶段,对象树被遍历,eden区中存活的对象被复制到空的survivor区中,原本survivor区中存活的对象如果年龄未达到阈值,年龄会加1,达到阈值会被复制到old区中,如果survivor空间不够,eden空间的部分数据会直接晋升到old区中。

  第五阶段,处理引用: 

  处理软引用、虚引用、弱引用等引用。最终Eden区的数据为空,GC停止工作,因为是复制清除算法,目标内存中的对象都是连续存储的,没有碎片。

  ②.回收过程二:老年代并发标记过程

 

 

  第一阶段:初始标记阶段:

  标记 GC Roots 直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC

  第二阶段:根区域扫描

  G1 GC在初始标价的存活区扫描对老年代的引用,并标记被引用对象,该阶段非STW,并且只有完成该阶段后,才能开始下一次STW年轻代垃圾回收。

  第三阶段:并发标记

  在整个堆中进行并发标记(非STW),此过程可能被young GC 中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

  第四阶段:最终标记

  由于应用程序持续进行,需要修正上一次的标记结果。是STW的,G1 GC清空SATB缓冲区,跟踪未被访问的存活对象,并执行引用处理。(G1采用了初始快照算法:snapshot-at-the-begining来解决并发标记中漏标的问题)

  第五阶段:独占清理

  此阶段需要STW,计算各个区域存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,为了下阶段做铺垫。(此阶段不会实际做垃圾回收)

  第六阶段:并发清理阶段

  识别并清理完全空间区域

  ③.混合回收Mixed GC

  Mixed GC 并不是Full GC,老年代的推占用率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的young和部分old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做Mixed GC,主要使用复制清除算法,需要把各个region中存活饿的对象复制到别的region里去,拷贝过程中如果发现没有足够的空的region就会触发一次Full GC。

 

  并发标记结束以后,老年代中百分百为垃圾的内存区域被回收,部分为垃圾的内存分段被计算出来,默认情况下,这些部分为垃圾的老年代的内存分段分8次(可以通过-xx:G1MixedGCCountTarget设置)被回收。

   混合回收的回收集(Collection Set) 包括八分之一的老年代内存区域,Eden区内存分段,Survivor区内存分段。混合回收算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。

  由于老年代中的内存分段默认是分8次回收,G1会优先回收垃圾多的内存区域。垃圾占本内存区域比例越高,越会被回收。并且有一个阈值会决定内存区域是否被回收-XX:G1MixedGCLiveThresholdbeiPercent,默认为85%,这个值的意思是存活对象占比低于85%才会被回收。如果垃圾占比太低,意味着存活对象多,在复制清除时候会花费更多时间。

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

  ④.G1可选过程四:Full GC

  G1的初衷就是避免Full GC的出现,但是如果上述方式不能正常工作,G1会STW,使用单线程的内存回收算法进行垃圾回收,性能非常差,应用程序停顿时间长。

  避免Full GC 的发生,一旦发生需要进行调整。比如推内存太小,当G1在复制存活对象没有空的内存区域可用,则会退回Full GC,这种情况可以通过增大内存解决。

  发生Full GC的原因可能有两个:

    1.回收的时候没有足够的区域来存放晋升的对象

    2.并发处理过程没完成,空间就耗尽了

  、G1回收器优化建议

  9.1 4种情况会触发G1 Full GC

  G1收集器同CMS收集器一样,在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,他仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别,整个应用STW,不能处理任何请求,这个是要尽量避免的。有时候会在垃圾回收日志中观察到Full GC,这些日志是一个信号,表明我们需要进一步调优才能提升应用程序的性能。主要有4种情况会触发Full GC,如下:

  ①. 并发模式失效

  G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这个情形下,需要增加堆大小,或者调整周期(例如增加线程数:-XX:ConcGCThreads等)。

  GC日志如下的示例:

  

 

   解决办法:发生这种失败意味着堆的大小应该增加了,或者G1收集器需要更早开始,或者调整线程数,让他运行更快。

  ②. 晋升失败 (to-space exhausted 或者 to-space overflow)

  G1收集器完成了标记阶段,开始启动混合垃圾回收,清理老年代分区,不过,老年代空间的垃圾回收释放出足够内存之前就被耗尽,即G1进行GC的时候没有足够的内存给存活对象或者晋升对象使用,由此触发一次Full GC。

  

 

   这种失败通常意味着混合式收集需要更迅速完成垃圾收集:每次新生代垃圾收集需要处理更多老年代的分区。

  解决这种问题的方式是:

  1.增加-XX:G1ReservePercent选项的值(并相应增加总的堆大小),这样老年代会预留更多的空间来给新生代的对象晋升

  2.通过减少-XX:XX:InitiatingHeapOccupancyPercent(启动并发GC周期时的堆内存占用百分比) 提前启动标记周期,启动并发GC周期时的堆内存占用百分比

  3.也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

  ③. 疏散失败(to-space exhausted 或者 to-space overflow)

    进行新生代垃圾回收时,Survivor空间和老年代中没有足够的空间容纳所有的幸村对象。这种情形在GC日志中体现:

  

    这条日志表明堆已经几乎完全用尽或者碎片化了。G1收集器会尝试修复这种失败,但是收效甚微,结果就是G1收集器会转化为使用Full GC

  解决这种问题的方式是:

  1.增加-XX:G1ReservePercent选项的值(并相应增加总的堆大小),这样老年代会预留更多的空间来给新生代的对象晋升

  2.通过减少-XX:XX:InitiatingHeapOccupancyPercent(启动并发GC周期时的堆内存占用百分比) 提前启动标记周期,启动并发GC周期时的堆内存占用百分比

  3.也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

  4.巨型对象分配失败

  当巨型Humongous对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量巨型对象,增加内存或者增大-XX:G1HeapRegionSize(设置每个region大小),使巨型对象不再是巨型对象。

  9.2 G1垃圾收集器调优

   1. G1垃圾收集器调优的主要目标是避免发生上述的情况,并发模式失败或者疏散失败,一旦发生这些失败就会导致Full GC。避免Full GC 的技巧也适用于频繁发生的新生代垃圾收集,这些垃圾收集需要等待扫描根分区完成才能进行。

  2.其次,调优可以使过程中的停顿时间最小化。

  下面列出能够避免发生Full GC的方法:

  • 通过增加总的堆空间或者调整老年代、新生代之间的比例来增加老年代空间大小。
  • 增加后台线程的数量(如果我们有足够的CPU)
  • 以更高的频率进行G1的后台垃圾收集活动。
  • 在混合式垃圾收集周期中完成更多的垃圾回收工作

  使用G1垃圾收集器时,XX:MaxGCPauseMillis参数有一个默认值:200毫秒。如果G1收集器发生停顿(STW)的时长超过该值,G1收集器就会尝试各种方式进行弥补:调整新生代与老年代比率,调整堆大小,更早地启动后台处理,改变晋升阈值,或者是在混合式垃圾收集周期中处理更多或者更少地老年代分区。

  通常的取舍就是发生在这里:如果减少XX:MaxGCPauseMillis参数值,为了达到停顿时间的目标,新生代的大小会相应减少,不过新生代垃圾收集的频率会更加频繁。除此之外,为了达到停顿时间的目标,混合式GC收集老年代分区也会减少,而这样会增大并发模式失败发生的机会。

  ① 调整G1垃圾收集的后台线程数

  为了让G1在垃圾回收更高效,可以尝试增加后台标记线程数量。对于应用线程暂停的周期(初始标记、最终标记等),可以使用ParallelGCThreads参数设置运行的线程数;对于并发阶段可以使用ConcGCThreads参数设置运行线程数。

  ② 调整G1垃圾收集器的运行频率

  如果G1更早的启动垃圾收集,也能提高效率。G1收集器通常在堆占用达到某个比率(通过参数 XX:InitiatingHeapOccupancyPercent 设定),默认值45,这个参数依据的是整个堆的使用情况而不是老年代的(这就与CMS不太一样)。

  ③ 调整G1收集器的混合式垃圾收集周期

  并发周期之后,老年代的标记分区回收完成之前,G1收集器无法启动新的并发周期。因此,让G1更早启动标记周期的另外一种方法是在混合式垃圾回收周期中尽量处理更多分区(如此一来最终的混合式GC周期就变少了)。

  混合式垃圾收集处理工作量取决3个因素:

  A. 有多少分区被发现大部分是垃圾对象。如果分区的存活对象占用低于85%,这个分区就被标记为可以进行垃圾回收;参数-XX:G1MixedGCLiveThresholdbeiPercent=85 即

  B. G1回收分区时最大混合式GC周期数,即执行几次混合回收,默认值为8次。意思是先停止系统运行,混合回收一些Region,再恢复系统运行,再停止系统运行,混合回收一些Region;往返重复8次。可以通过参数-XX:G1MixedGCCountTarget=8
  9.3 常见调优参数

  -XX:MaxGCPauseMillis=n (默认200毫秒)

  前面介绍过使用GC的基本的参数:

  -XX:+UseG1GC -Xms32g -XX:MaxGCPauseMillis=200

  前面2个参数好理解,使用G1垃圾收集器,堆大小为32g,后面 XX:MaxGCPauseMillis 参数该如何配置,从字面意思上看,就是允许GC的最大暂停时间。G1尽量确保每次GC暂停的时间都在设置的XX:MaxGCPauseMillis 范围内。那G1是如何做到最大暂停时间的呢?这涉及到另外一个概念CSet(collection set)。它的意思是在一次垃圾收集器中将被收集的区域集合。

  在 Young GC 的时候,选定所有新生代里的 Region 为被回收的区域,通过控制新生代的region个数来控制young gc的开销。

  在 Mixed GC 的时候,选定所有新生代里的 Region,外加根据global concurrent marking(全局并发标记)统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。

  在理解了上述这些后,再设置最大暂停时间就好办了。首先,能容忍的最大暂停时间是有一个限度的,需要在这个限度内设置,应该设置多少呢?需要再吞吐量跟MaxGCPauseMillis之间做平衡。如果MaxGCPauseMillis设置过小,那么GC就会频繁,吞吐量下降。如果MaxGCPauseMillis设置过大。那么应用程序暂停使劲按就会变长。可以依据G1默认值200毫秒,然后与我们实际情况做对比,再来做调整。

  9.4 其他调优参数

  -XX:G1HeapRegionSize=n

  设置的 G1 区域的大小。值是2次幂,范围是1MB到32MB之间。目标是根据最小的JAVA堆大小划分出约2048个区域。

  -XX:ParallelGCThreads=n

  设置STW工作线程数的值。将n的值设置为逻辑处理器的数量。n的值与逻辑处理器数量相同,最多为8。

  -XX:ConcGCThreads=n

  设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。

  ConcGCThreads=(ParallelGCThreads + 2) /4

  -XX:InitiatingHeapOccupancyPercent=45

  设置触发标记周期的java堆占用率阈值。默认占用率是整个java堆的45%。

  该值设置太高,会陷入Full GC泥潭中,因为并发阶段没有足够的时间在剩下堆空间被填满之前完成垃圾收集。如果设置太小,应用程序又会以超过实际需要的节奏进行大量的后台处理。

  避免使用以下参数:-Xmn或者-XX:NewRatio等其他相关选项显示设置年轻代大小,固定年轻代的带下会覆盖暂停时间目标。

  -XX:G1MixedGCLiveThresholdbeiPercent=85

  为活跃度阈值,不同版本默认值不同,有65%和85%。在全局并发标记阶段,如果一个Region的存活对象的空间占比低于此值,则会被纳入Cset。此值直接影响到Mixed GC选择回收的区域,当发现GC时间较长时,可以尝试调低此阈值,尽量优先选择回收垃圾占比高的Region,但此举也可能导致垃圾回收的不够彻底,最终触发Full GC,就是如果大量短存活的对象占用了区域中大部分空间,这个时候不会回收掉此对象,久而久之就会堆被占满,触发Full GC。

  -XX:G1MixedGCCountTarget=8

  设置标记周期完成后,对存活对象低于G1MixedGCLiveThresholdbeiPercent的老年代区域执行多少次混合垃圾回收。默认值是8次混合垃圾回收。

  -XX:G1OldCSetRegionThresholdPercent=10

  设置混合垃圾回收期间要回收的最大老年代区域数。默认是10%,也就是每轮Mixed GC附加的Cset的Region不超过全部Region的10%,最多10%,如果暂停时间短,那么可能会少于10%。一般这个值不需要额外调整。

  -XX:G1ReservePercent=10

   设置G1为分配担保预留的空间比例,也就是老年代会预留10%的空间来给新生代的对象晋升,如果经常发生新生代晋升失败而导致Full GC,那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间。

  -XX:G1HeapWastePercent=5

   愿意浪费的堆百分比。是触发 Mixed GC 和退出 Mixed GC 的条件。 当collection set中的垃圾对象占总堆分配空间的5%以上(在jdk11中)时, Mixed GC会被触发,而退出Mixed GC的充分条件之一就是 collection set中的垃圾对象率低于5%。
posted @ 2022-10-19 17:36  梅晓煜  阅读(1518)  评论(0编辑  收藏  举报