[JVM垃圾回收4]垃圾回收器

 

GC分类与性能指标

垃圾回收器的概述

  • 垃圾收集器没有在规范中进行过多规定,可以由不同厂商,不同版本JVM来实现。
  • 由于JDK的版本处于高速迭代,因此已经衍生众多GC版本。
  • 从不同角度分析垃圾收集器,可以将GC分为不同的类型。

GC分类

  • 线程(垃圾回收线程1)分,分为串行垃圾回收器和并行垃圾回收器

77ef086625b116e9fbe3967842197fb3.png

  • 工作模式分,可分为并发式垃圾回收器和独占式垃圾回收器

9d0e0f756969a03cc6bd6ae045f25883.png

  • 碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器
    • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片
    • 非压缩式垃圾回收器不进行这步操作
  • 工作区间分,可分为年轻代垃圾回收器和老年代垃圾回收器

评估GC的性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例
    • 总运行时间 = 程序运行时间 + 内存回收时间
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。
  • 内存占用:Java堆区所占的内存大小。
  • 快速:一个对象从诞生到被回收所经历的时间。

以上标出三者共同构成一个“不可能三角”。三者总体表现会随着技术进步而越来越好。一个优秀的收集器通常最多同时满足其中两项。

简单来说,主要抓住两点:

  • 吞吐量
  • 暂停时间

吞吐量(throughput)

  • 吞吐量就是CPU用于运行用户代码的时间与CPU总耗时的比值
    • 比如:虚拟机总运行100分钟,其中垃圾收集划掉1分钟,吞吐量就是99%
  • 这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长时间的基准,快速响应是不必考虑的。
  • 吞吐量优先,意味着在单位时间内,STW的时间最短

暂停时间(pause time)

  • 暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态
  • 暂停时间优先,意味着尽可能让单次STW的时间最短
    c9ec2d004ddbba7f3ccd3b265d249721.png

吞吐量vs暂停时间

 

  • 高吞吐量较好因为会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作,直觉上,吞吐量越高程序运行越快。
  • 低暂停时间(低延迟)较好是因为从最终用户角度来看不管是GC还是其他原因导致一个应用程序被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的200ms暂停都可能打断终端用户的体验。因此,具有低的较大暂停时间是非常重要的,特别是对于交互式应用程序
  • 不幸的是,“高吞吐量”和“低暂停时间”是互相矛盾的。
    • 因为如果选择吞吐量优先,必然需要降低内存回收执行频率,但这样导致GC需要更长时间进行回收。
    • 想法,如果选择低延迟优先,为了降低每次执行回收的暂停时间,只能频繁地执行内存回收,但这又导致年轻代内存的缩减和导致吞吐量下降。
  • 在设计GC算法时,必须明确目标,或者找到二者折中。
  • 现在标准:在最大吞吐量优先的情况下,降低停顿时间

不同的垃圾回收器概述

GC发展史

有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection ,对应的产品我们称为 Garbage Collector 。

  • 1999年,随JDK 1.3.1 一起来的是串行方式的 Serial GC ,它是第一款GC。ParNew GC 是 Serial GC 的多线程版本。
  • 2002年2月26日,Parallel GC 和 Concurrent Mark Sweep GC( 即 CMS ) 跟随 JDK1.4.2 一起发布。
  • Parallel GC 在 JDK6 之后成为 Hotspot 默认GC。
  • 2012年,在 JDK1.7u4 中,G1 可用。
  • 2017年,JDK9 中 G1 成为默认垃圾收集器,以替代 CMS。
  • 2018年3月,JDK10 中 G1 的并行完整垃圾回收,实现并行性能改善最坏情况的延迟。
  • 2018年9月,JDK11 发布。引入 Epsilon GC ,又称为“No-Op(无操作)” 回收器;同时引入 ZGC: 可伸缩的低延迟回收器(Experimental)。
  • 2019年3月,JDK12 发布。增加 G1,自动返回未使用堆内存给操作系统; 同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。
  • 2019年9月,JDK13 发布。增强 ZGC,自动返回未使用堆内存给操作系统。
  • 2020年3月,JDK14 发布。删除 CMS。扩展 ZGC 在 mac 和 windows 的应用。

7款 经典 的垃圾回收器(不包含新发行的GC)
分类:

  • 串行回收器: Serial 、Serial Old
  • 并行回收器: ParNew、Parallel Scavenge、 Parallel Old
  • 并发回收器: CMS、 G1
    与分代的关系:
  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:Serial Old、Parallel Old、CMS
  • 整堆收集器:G1

832264297b316737208b6ef8e57e3ed6.png

垃圾收集器的组合关系

b0af83a355388a2cf232a262cc25be5f.png

  1. 两个收集器有连线,表明它们可以搭配使用:
    Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel、G1

  2. 其中Serial Old作为CMS出现:Concurrent Mode Failure失败的后备方案

  3. (红色虚线)由于维护和兼容性测试版本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP 173),并在JDK9中完全去取消了这些组合的支持(JEP214)

  4. (绿色虚线)JDK 14中:弃用Parallel Scavenge和Serial Old GC组合(JEP366)

  5. (青色虚线)JDK 14中:删除CMS垃圾回收器(JEP363)

  • Java使用的场景很多,移动端,服务端。所以需要对不同场景,提供不同的垃圾收集器,提高垃圾收集性能。选择的只是对具体应用最合适的收集器

如何查看默认垃圾收集器

  • -XX:+PrintCommandLineFlags:查看命令行相关参数,(包含使用的垃圾收集器)
  • 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

 

Serial回收器:串行回收

  • Serial收集器是最基本、历史最悠久的垃圾收集器。JDK1.3之前新生代唯一的选择。

  • Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。

  • Serial收集器采用复制算法、串行回收和STW机制的方式执行内存回收

  • 除了年轻代之外,Serial收集器还提供执行老年代垃圾收集的Serial Old收集器。Serial Old收集器同样也采用了串行回收和STW机制,只不过内存回收算法使用标记-压缩算法

    • Serial Old是运行在Client模式下默认的老年代垃圾收集器
    • Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用②作为老年代CMS收集器的后备方案edf515faf9380713d1f6812ec0a84565.pngedf515faf9380713d1f6812ec0a84565.png
      这个收集器是一个单线程的收集器,但它的“单线程”的意义并不是说明只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是在垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(STW)。
  • 优势:简单而高效(与其他收集器单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互开销,可获得最高的单线程收集效率

    • 运行在Client模式下的虚拟机是个不错的选择
  • 在用户桌面应用场景,可用内存一般不大,可以在较短时间内完成垃圾收集(几十ms到一百多ms)

  • 在HotSpot虚拟机中,使用 -XX:+UseSerialGC 参数可以是定年轻代和老年代都使用串行收集器

    • 等价于新生代使用Serial GC,且老年代使用Serial Old GC

测试代码:

public class GCTest {

    /**
     * -XX:+PrintCommandLineFlags -XX:+UseSerialGC
     * @param args
     */
    public static void main(String[] args) {
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

3e2f5913c050b1a4f4f4febebbfee1d7.png


总结:

  • 这种垃圾收集器现在已经不使用。而且在限定单核CPU才可以用。
  • 对于交互式较强的应用而言,这种垃圾收集器是不能接受的。一般在Java Web应用程序中不会采用这种串行垃圾收集器。

 

ParNew回收器:并行回收

  • 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。
    • Par是Parallel的缩写,New:只能处理新生代
  • ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代通用采用复制算法,STW机制
  • ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器。
    2b08c073f2f85e497a7e250c096e08fc.png
  • 对于新生代,回收次数频繁,使用并行方式高效
  • 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
  • 由于ParNew收集器是基于并行回收,那么可否判定ParNew收集器在任何场景都比Serial收集器效率高?
    • ParNew收集器运行在多CPU的环境下,可以充分利用多CPU、多核心等物理资源优势,更快速完成垃圾收集,提升程序的吞吐量。
    • 但在单个CPU环境下,ParNew收集器不比Serial收集器更高效
  • 除了Serial外,目前只有ParNew GC能与CMS收集器配合工作
  • 在程序中,可通过“-XX:+UseParNewGC”手动指定ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
  • -XX:ParellelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数

Parallel回收器:吞吐量优先

  • HotSpot的年轻代除了拥有ParNew收集器是基于并行回收以外,Parallel Scavenge收集器同样采用了复制算法、并行回收和STW机制
  • Parallel收集器的出现是否多此一举?
    • 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
    • 自适应调节策略也是Parallel Scavenge与ParNew的一个重要区别。
  • 高吞吐量则可以高效利用CPU时间,尽快完成程序的运算任务,主要**适合在后台运算不需要太多交互的任务。**因此,常见在服务器环境中使用。例如:那些执行批量处理、订单处理、工资支付、科学计算的应用程序
  • Parallel收集器在JDK1.6时提供了用户执行老年代垃圾收集器Parallel Old收集器,替换老年代Serial Old收集器。
  • Parallel Old收集器采用标记-压缩算法,通用基于并行回收和STW机制
    cd30c7fdbf13c0ad65c0977fc96c4373.png
  • 在程序吞吐量优先的场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下内存回收性能很不错
  • 在Java8中,默认是此垃圾收集器

参数配置:

 

  • -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收
  • -XX:+UseParallelIOldGC 手动指定老年代都是使用并行回收收集器。
    • 分别适用于新生代和老年代,默认jdk8是开启的
    • 上面两个参数,默认开启一个,另一个也会被开启(互相激活)
  • -XX:+ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多线程影响垃圾收集性能。
    • 默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量
    • 当CPU数量大于8个,ParallelGCThreads的值等于 3 + [5 * CPU_COUNT/8]
  • -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间,即STW时间,单位毫秒。
    • 为了尽可能把停顿时间控制在MaxGCPauseMillis以内,收集器在工作时会调整Java堆大小或者其他参数。
    • 对用户来讲,停顿时间越短体验越好。但在服务端,注重高并发,整体的吞吐量。所以服务端适合Parallel,进行控制。
    • 该参数使用需谨慎
  • -XX:GCTimeRatio 垃圾收集时间占总时间的比例(=1 / (N+1) )用户衡量吞吐量大小。
    • 取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%
    • 与前一个 -XX:MaxGCP auseMillis 有一定矛盾性。暂停时间越长,Ratio参数就容易超过设定比例。
  • -XX:+UseAdaptiveSizePolicy 设置Parallen Scavenge收集器具有自适应调节策略
    • 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
    • 在手动调优比较困难的场合,可以直接使用自适应的方式,仅指定虚拟机的最大堆、目标吞吐量和停顿时间,让虚拟机自己完成调优。

 

CMS回收器:低延迟

  • 在JDK1.5时期,HotSpot推出了一款在强交互应用中几乎认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作
  • CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合用户交互的程序,良好的响应速度能提升用户体验。
    • 目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户较好体验。CMS收集器就非常符合这类要求。
  • CMS的垃圾收集算法采用标记-清除算法,并且也会STW
  • 不幸的是,CMS作为老年代收集器,却无法与JDK1.4中存在的新生代收集器Parallel Scavenge配合使用,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
  • 在G1出现之前,CMS使用非常广泛。目前仍有许多系统使用CMS GC。

f827a4562024e4a459f2f5206ae2b08c.png
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记、并发标记、重新标记和并发清除阶段。

  • 初始标记(Initial-Mark)阶段:在这个阶段中,程序会STW,这个阶段主要任务仅仅是标记GC Roots能直接关联的对象。一旦完成标记就会退出STW。由于直接关联对象比较小,所以这里速度非常快
  • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集器线程一起并发运行。
  • 重新标记(Remark)阶段:由于并发标记阶段中,程序的工作线程和垃圾收集线程交叉运行,因此为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段长一些,但也远远比并发标记阶段时间短。
  • 并发清除(concurrent-sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

尽管CMS收集器采用的是并发式回收(非独占式),但在其初始化标记和再次标记着两个阶段仍需要STW,不过STW时间不会太长,因此可以说明目前所有垃圾收集器都做不到完全不需要STW,只能尽可能缩短暂停时间。

由于最耗费时间的并发标记阶段和并发清除阶段都不需要暂停工作,所以整体的回收还是低停顿的

另外,由于在垃圾收集阶段没有STW,所以在CMS回收过程中,还应该确保应用程序的用户线程有足够内存可用,因此,CMS不能像其他收集器那样等到老年代几乎被填满时才进行收集,而是当堆内内存使用率达到某一阈值时,便开始进行回收,以确保应用程序CMS工作时仍有足够空间支持程序运行。要是CMS运行期间预留内存无法满足程序,就会出现一次 “Concurrent Mode Failure“ 失败,这时虚拟机启动后备预案:临时启用Serial Old收集器重新进行老年代垃圾收集,这样停顿时间就很长了。

CMS收集器使用标记-清除算法,意味着每次回收后,内存空间极有可能是不连续的,不可避免地产生内存碎片,因此CMS在为新对象分配空间时,无法使用指针碰撞,而只能选择空闲列表。

 

Q:Mark Sweep会造成内存碎片,为什么不缓存Mark Compact算法?
A:因为Compact整理内存的话,原来用户线程的内存就无法使用。Mark Compact更适合STW场景下使用。


  • CMS的有优点:

    • 并发收集
    • 低延迟
  • CMS的弊端:

    • 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
    • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程导致应用程序变慢,总吞吐量会降低。
    • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure“失败而导致一次Full GC的产生,在并发标记阶段由于工作线程和垃圾收集线程是同时或交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次GC执行时来释放之前未被回收的内存空间。

CMS参数设置:

  • -XX:+UseConcMarkSweepGC 手动指定使用CMS收集器执行内存回收任务
    • 开启参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+CMS(Old区)+Serial Old的组合。
  • -XX:CMSInitiatingOccupanyFraction 设置堆内存使用率阈值,一旦到达该阈值,便开始进行回收。
    • JDK5及之前版本默认是68,即老年代内存空间使用率达到68%时,执行一次CMS回收。JDK6及以上版本默认值92%
    • 如果内存增长缓慢,可以设置一个较大的值,大阈值有效降低CMS触发频率,减少老年代回收次数明显改善应用程序性能。反之,如果应用程序内存使用率增长很快,可以降低这个阈值,避免触发老年代串行收集器。因此通过此选项有效降低Full GC执行次数。
  • -XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所以带来的问题就是停顿时间更长了。
  • -XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理。
  • -XX:ParallelCMSThreads 设置CMS线程数量
    • CMS默认启动线程是 (ParallelGCThreads + 3)/4 , ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能非常糟糕。

小结

以下口令:
如果想最小化使用内存和并行开销:选择Serial GC
如果想最大化应用程序吞吐量,选择Parallel GC
如果想最小化GC的中断或停顿时间,选择CMS GC

JDK后续版本CMS的变化

  • JDK9新特性:CMS被标记为Deprecate(JEP291)
    • 如果对JDK9及以上版本HotSpot虚拟机使用参数 -XX:+UseConcMarkSweepGC 来开启CMS收集器,会收到警告信息
    • JDK14新特性:删除CMS垃圾回收器(JEP363)如果在JDK14中使用 -XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,但是不会exit。JVM会自动退回默认GC方式启动JVM

 

G1回收器:区域化分代式

G1(Garbage First)是JDK7 update4之后引入的新的垃圾收集器,为了适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。


为何叫Garbage First(G1)?

  • G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续)。使用不同的Region来表示Eden、S0、S1、OldGen等。
  • G1 GC有计划地避免在整个Java堆中进行全区域垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
  • 由于这种方式侧重点在于回收垃圾最大量的区间(Region),所以G1起名:垃圾优先(Garbage First)

G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停段时间的同时,还兼具高吞吐量的性能特征。

在JDK1.7版本正式启用,育出了Experimental表示,是JDK 9以后默认垃圾回收器,取代了CMS回收器以及Parallel+Parallel Old组合,Oracle官方称为 全功能垃圾收集器

与此同时,CMS已经在JDK 9中被标记为废弃(deprecated)。在JDK8中还不是默认垃圾回收器,需要使用 -XX:+UseG1GC来启用。

G1回收器的特点

优势:

  • 并行与并发

    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
    • 并发性:G1拥有与程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • 分代收集

    • 从分代上看,G1仍然属于分代垃圾收集器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
    • 堆空间分为若干个区域(Region),这些区域中包含了逻辑上的老年代和年轻代
    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。而其他回收器,或者工作在年轻代,或者工作在老年代。
      648b157c5575a43065d1c6aa20d4bab8.png
  • 空间整合

    • CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
    • G1将内存划分为一个个Region。内存回收是以Region为基本单位。Region之间是复制算法,但整体上可看做是标记-压缩算法,这两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象不会因为无法找到连续空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
  • 可预测的停顿时间模型(即:软实时 Soft real-time)
    这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

    • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿的情况的发生得到较好控制
    • G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先级列表,每次根据允许收集时间,优先回收价值最大的Region。保证了G1收集器在有限时间内可以获取尽可能高的收集效率
    • 相比于CMS GC,G1未必能做到CMS在最好情况下的停顿,但是最差情况要好很多。

缺点:

相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序执行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序停顿时的额外执行负载(Overload)都要比CMS高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

G1回收器参数设置

  • -XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务
  • -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出越2048个区域。默认是堆内存的1/2000
  • -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不能保证达到)。默认值是200ms
  • -XX:ParallelGCThreads 设置STW时GC线程数的值。最多设置为8
  • -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
  • -XX:InitiatingHeapOccupancyPerecent 设置触发并发GC周期的Java堆占用率阈值,超过此值就触发GC。默认值是45。

G1的设计原则就是简化JVM性能调优,开发人员只需要三个简单步骤就可完成

  1. 第一步,开启G1垃圾收集器
  2. 第二步,设置堆的最大内存
  3. 第三步,设置最大的停顿时间

G1中提供了三种垃圾回收模式:YoungGC。MixedGC和Full GC,在不同条件下被触发。

G1回收器的适用场景

  • 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆内表现并不惊喜)

  • 最主要的应用场景是需要低GC延迟,并具有大堆的应用程序提供解决方案

  • 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒(G1通过每次只清理一部分而不是全部Region的增量式清理来保证每次GC停顿时间不会太长)

  • 用来替换JDK1.5的CMS收集器

    • 在下面情况时,使用G1比CMS好
      • 超过50%的Java堆被活动数据占用
      • 对象分配频率或年代提升频率很大
      • GC停顿时间过长(长于0.5秒或1秒)
  • HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度较慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

Region:化整为零

使用G1收集器时,它将整个Java堆划分为越2048个大小相同的独立Region块,每个Region块大小根据堆空间实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变

虽然还保留新生代和老年代概念,但新生代和老年代不再是物理隔离了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
6284d9dbca8a535aee282560db238348.png

  • 一个Region有可能属于Eden,Survivor或者Old/Tenured内存区域。但一个region只有可能属于一个角色。空白表示未使用的内存空间。
  • G1还增加了一种新的区域,叫做Humongous内存区域,如图中H块。主要用于存储大对象,如果超过1.5个Region,就放到H。

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

G1垃圾回收过程

G1 GC的垃圾回收主要包括下面三个环节

  • 年轻代GC(Young GC)

  • 老年代并发标记过程(Concurrent Marking)

  • 混合回收(Mixed GC)

  • (如果需要,单线程、独占式。高强度Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收)
    ff62c21fa55116c804697b9f2fe01afc.png

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

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

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


Remembered Set记忆集

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

解决办法:

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

G1回收过程一:年轻代GC

JVM启动时,G1会先准备好Eden区,程序在运行过程中不断创建对象到Eden,当Eden耗尽,G1启动一次年轻代垃圾收集。
年轻代垃圾收集只会回收Eden区和Survivor区
YGC时,G1会STW,G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含Eden区和Survivor区所有的内存分段。
4228958b605d057cd9861f08b8b88355.png

开始如下过程:

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

备注:对于应用程序的引用赋值语句object.field=object,JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了引用信息的card。在年轻代会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确反映引用关系。之所以不在引用赋值语句处直接更新RSet是因为性能需要,RSet的处理需要线程同步,开销会很大,使用队列性能更好。

G1回收过程二:并发标记过程

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

G1回收过程三:混合回收

863cd6d78b618fcbcaab86425e925976.png

  • 并发标记结束后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算出来。默认情况下,这些老年代的内存分段会分8次(可以通过 -XX:G1MixedGCCountTarget设置)被回收。
  • 混合回收的回收集(Collection Set)包括1/8的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法一样,只是回收集多了老年代的内存分段。具体过程参考上面年轻代回收过程。
  • 由于老年代的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段,垃圾占内存分段比例越高,越会被先回收。并且有一个阈值会决定内存分段是否被回收, -XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例达到65%才会被回收。如果垃圾占比太低,意味着存活对象占比高,复制的时候会花费更多时间。
  • 混合回收并不一定要进行8次,有一个阈值 -XX:G1HeapWastePercent 默认值10%,意思是允许整个堆中有10%被浪费,意味着如果发现可以回收的垃圾占内存比例低于10%,则不再进行混合回收。因为GC会花费很多时间但回收到的内存却很少。

G1回收可选过程四:Full GC

G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会STW,使用单线程的内存回收算法进行垃圾回收,性能非常差,停顿时间长。
要避免Full GC的发生,一旦发生需要进行调整。比如堆内存太小,当G1在复制存活对象时没有空的内存分段使用,则会回退Full GC。
导致Full GC的原因可能有两个:

  1. Evacuation的时候没有足够to-space存放晋升对象
  2. 并发处理过程完成之前空间耗尽

G1补充

从Oracle官方透露信息可知,回收阶段(Evacuation)其实也有想过设计成和用户程序一起并发执行,但这件事做起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控制的,所以并不迫切去实现,而选择把这个特性放到低延迟垃圾收集器ZGC中。另外,还考虑到G1不仅仅面向低延迟,停顿用户线程能最大幅度提高垃圾收集效率,为了保证吞吐量才选择完全暂停用户线程的实现方案。

G1优化建议

  • 年轻代大小

    • 避免使用 -Xmn 或 -XX:NewRatio等相关选项显示设置年轻代大小
    • 固定年轻代的1大小会覆盖暂停时间目标
  • 暂停时间的目标不要太过苛刻

    • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
    • 评估G1 GC的吞吐量时,暂停时间目标不要过于苛刻。目标太苛刻表示愿意承受更多垃圾回收开销,而这些会直接影响吞吐量。

垃圾回收器总结

31389b366c9858cbdffcfa11880a150f.png

cd08d223e9fe73b3eb994103adf48905.png

怎么选择垃圾回收器

  • Java垃圾收集器的配偶之对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM性能有一个很大提升。

  • 怎么选择垃圾收集器:

    1. 优先调整堆的大小让JVM自适应完成
    2. 如果内存小于100M,使用串行收集器
    3. 如果是单核、单机程序,并且没有停顿时间要求,串行收集器
    4. 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
    5. 如果是多CPU、追求停顿时间,需要快速响应(比如延迟不超过1秒,如互联网应用),使用并发收集器,官方推荐G1,性能高。现在互联网项目,基本都是使用G1
  • 没有最好的收集器,更没有万能的收集器

  • 调优永远针对特点场景、特点需求,不存在一劳永逸的收集器



posted @ 2020-08-12 20:43  OldCha  阅读(203)  评论(0)    收藏  举报