[解密JVM-16] 垃圾回收:垃圾回收器

1、GC 分类与性能指标

1.1 垃圾回收器的分类

  按线程分,可以分为串行垃圾回收器和并行垃圾回收器。
在这里插入图片描述
  按工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。并发式的特点是垃圾回收器和应用程序线程交替工作,以尽可能减少应用程序的停顿时间。独占式的特点是垃圾回收器一旦运行,必须停止所有的用户线程,直到垃圾回收过程结束。
在这里插入图片描述

1.2 垃圾回收器的性能指标

  1、吞吐量:运行用户代码的时间占总运行时间的比例,总运行时间为程序运行的时间和内存回收的时间之和。可以理解为有效值。

  2、垃圾收集开销:吞吐量的补数,即垃圾收集所用时间与总运行时间的比值。可以理解为额外开销的值(站在用户线程的视角)。

  3、暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间(STW)。

  4、收集频率:相对于应用程序的执行,垃圾收集操作发生的频率。

  5、内存占用:Java 堆区中被垃圾收集器所占用的内存大小。

  6、快速:一个对象从诞生到被回收所经历的时间。

  其中,吞吐量、暂停时间和内存占用共同构成一个“不可能的三角”。三个指标的表现会随着技术的发展而越来越好,一款优秀的垃圾回收器通常最多同时满足其中两项。

  这三个指标中,暂停时间的重要性日益凸显,因为随着硬件的发展,内存占用多越来越不足轻重,硬件性能的提升也有助于降低垃圾回收器对应用程序的影响,即吞吐量也提高了。但是内存的增大,反而会增大暂停时间,就好像屋子大了,清理垃圾也更麻烦了。

  对比垃圾回收器的性能,主要从吞吐量和暂停时间来把握。

  吞吐量 = 用户代码运行时间 / ( 用户代码运行时间 + 垃圾收集时间 )。

  比如,虚拟机总共运行了 100 分钟,垃圾收集花费了 1 分钟,那么吞吐量就是 99%。

  这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。

  吞吐量优先意味着在单位时间内,STW的时间最短。如下图。
在这里插入图片描述
  暂停时间指的是一段时间内应用程序线程暂停,让 GC 线程工作的状态。

  例如,GC 期间 100 ms 的暂停时间意味着这 100 ms 内没有其他应用线程是活动的。

  暂停时间优先意味着尽可能让单次 STW 的时间最短。如下图。
在这里插入图片描述
  高吞吐量较好因为这会让用户程序的最终用户感觉只有应用线程在做“生产性”的工作,直觉上来说,吞吐量越高,程序运行越快。

  低暂停时间较好是因为从最终用户的角度来看,不管 GC 还是其他原因导致 STW 始终是不好的,这取决于应用程序的类型,有时间甚至短暂的 200ms 暂停都可能打断终端用户的体验,比如王者荣耀打团的时候来个垃圾回收用了 200 ms,对面敌人的手机垃圾回收用了 20 ms,你觉得打团打得过么?因此,具有较低的暂停时间是非常重要的,特别是对于一个交互式应用程序。

  但是!高吞吐量和低延时是一对矛盾。

  如果高吞吐量优先,那么会降低内存的回收频率,导致 GC 需要更长的暂停时间来垃圾回收。

  相反,低延时情况下,为了降低暂停时间,只能频繁执行 GC,但这又引起了年轻代内存的缩减(占用部分内存保存上下文信息)和导致程序吞吐量的下降(切换线程造成额外的时间消耗)。

2、不同的垃圾回收器概述

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

  1999 年,随着 JDK1.3.1 诞生的是串行方式的 Serial GC,它是第一款 GC,ParNew GC 是它的多线程版本;

  2002 年 2 月 26 日,随着 JDK1.4.2 一起发布的是 Parallel GC 和 Concurrent Mark Sweep GC;

  在 JDK6 之后,HotSpot 的默认 GC 就是 Parallel GC;

  2012 年,在 JDK1.7u4 版本中,G1 可用;

  2017 年,G1 变成 JDK9 的默认垃圾收集器,以代替 CMS;

  2018 年 3 月,在 JDK10 中 G1 垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟;

  2018 年 9 月,JDK11 发布,引入 Epsilon GC,又称为 No-Op,同时引入 ZGC;

  2019 年 3 月,JDK12 发布,增强 G1,自动返回未用堆内存给操作系统,同时引入 Shenandoah GC;

  2019 年 9 月,JDK13 发布,增强 ZGC,自动返回未用堆内存给操作系统;

  2020 年 3 月,JDK14 发布,删除 CMS 垃圾回收器,拓展 ZGC 在 macOS 和 Windows 上的应用。
在这里插入图片描述
  新生代收集器:Serial、ParNew、Parallel Scavenge;
  老年代收集器:Serial Old、Parallel Old、CMS;
  整堆收集器:G1。
在这里插入图片描述
  垃圾收集器的组合关系见下图:
在这里插入图片描述
  说明:
  1、有连线说明可以搭配使用,比如:Serial + CMS、Serial + Serial Old、ParNew + CMS、ParNew + Serial Old、Parallel Scanvenge + Serial Old、Parallel Scanvenge + Parallel Old;
  2、其中 Serial Old 作为 CMS 出现“Concurrent Mode Failure”失败的后备方案。
  3、红色虚线的组合在 JDK8 中声明为废弃,并在 JDK9 中完全取消这些组合;
  4、绿色虚线的组合在 JDK14 中弃用;
  5、绿色虚线方框在 JDK14 中删除了,图中的 CMS GC。

  为什么要这么多垃圾回收器?一个不够用么?

  这是因为 Java 的使用场景很多,移动端、服务器等,所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。

  虽然我们会对各个垃圾收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。

3、Serial GC:串行回收

3.1 简介

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

  Serial GC 是 HotSpot 中 Client 模式下的默认新生代垃圾收集器。

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

  除了年轻代之外,Serial GC 还提供用于执行老年代垃圾收集的 Serial Old GC,它同样采用了串行回收和 STW 机制,不同在于算法是用的标记-压缩算法。

  Serial Old GC 是运行在 Client 模式下默认的老年代的垃圾回收器。

  Serial Old GC 在 server 模式下主要两个用途:与年轻代的 Parallel Scavenge GC 配合使用、作为老年代 CMS GC 的后备垃圾回收方案。
在这里插入图片描述

3.2 工作方式

  Serial GC 和 Serial Old GC 的工作方式如下图:
在这里插入图片描述
  可以看出,它们是单线程的收集器,但它的“单线程”的意义有两个:
  1、只会使用一个 CPU 或一条收集线程去完成垃圾收集工作;
  2、进行垃圾回收时,必须暂停其他工作线程,直到收集结束。

  Serial GC 和 Serial Old GC 的优势在于简单而高效,对于限定单个 CPU 的环境来说,Serial GC 由于没有线程交互的开销,专心做垃圾收集工作,自然可以获得最高的单线程收集效率。

  运行在 Client 模式下的虚拟机是一个不错的选择。

  在用户的桌面应用场景中,可用内存一般不大(几十 MB 到 一两百 MB),可以在较短时间内完成垃圾收集(房子小好打扫),只要不频繁发生垃圾收集,使用串行回收器是可以接受的。

3.3 可设置参数

  在 HotSpot 中,使用参数-XX:+UseSerialGC可以指定年轻代和老年代都使用串行收集器。

  等价于:年轻代用 Serial GC,老年代用 Serial Old GC。

4、ParNew GC:并行回收

4.1 简介

  如果说 Serial GC 是年轻代中的单线程垃圾收集器,那么 ParNew GC 就是 Serial GC 的多线程版本。

  Par 是 Parallel 的缩写,New:只能处理年轻代。

  ParNew GC 除了采用并行回收的方式执行垃圾收集外,和 Serial GC 几乎没有任何区别,ParNew GC 同样采用复制算法和 STW 机制。

  ParNew 是很多 JVM 在 Server 模式下年轻代的默认垃圾收集器。

4.2 工作方式

在这里插入图片描述
  对于年轻代,回收频繁,使用并行的方式高效;
  对于老年代,回收不频繁,使用串行方式节省资源。(CPU 并行需要切换线程,串行可以省去切换线程的资源)

  由于 ParNew GC 是基于并行回事,那么是否可以断定 ParNew GC 在任何情况下的效率都比 Serial GC 高呢?

  ParNew GC 是运行在多 CPU 或多核环境下,可以充分利用 CPU 、多核心的硬件资源优势,可以更快完成垃圾收集,提高程序的吞吐量。

  但是在单个 CPU 环境下,ParNew GC 并不比 Serial GC 效率高,虽然 Serial GC 是基于串行回收,但是 CPU 不需要频繁做任务切换,可以有效避免多线程交互过程产生的一些额外开销。

  除了 Serial GC 外,只有 ParNew GC 能和 CMS GC 配合工作。

5、Parallel Scavenge GC:吞吐量优先

5.1 简介

  HotSpot 的年轻代除了拥有 ParNew GC 是基于并行回收的以外,Parallel Scavenge GC 同样采用了复制算法、并行回收和 STW 机制。

  那么 Parrallel Scavenge GC 的出现是否多此一举?

  显然不是的,Parrallel Scavenge GC 和 ParNew GC 的区别在于,Parrallel Scavenge GC 的目标是达到一个可控制的吞吐量,它也被称为吞吐量优先的垃圾收集器。

  自适应调节策略也是 Parrallel Scavenge GC 与 ParNew 的一个重要区别。

  高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运行任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,哪些执行批量处理、订单处理、工资支付、科学计算的应用程序。

  Parrallel Scavenge GC 在 JDK1.6 时提供了用于执行老年代垃圾收集的 Parallel Old GC ,代替老年代的 Serial Old GC。

5.2 可设置参数

  -XX:+UseParallelGC参数用于指定年轻代使用 Parallel GC。

  -XX:UseParallelOldGC参数用于指定老年代都是使用 Parallel Old GC。

  上面两个参数分别适用于年轻代和老年代,JDK8 是默认开启的,开启了其中一个,另一个也跟着开启(互相激活)。

  -XX:ParallelGCThreads设置年轻代并行收集器的线程数。一般地,最好和 CPU 数量相等,以避免过多的线程数影响垃圾收集的性能。

  在默认情况下,当 CPU 数量小于 8 个,这个参数值等于 CPU 的数量。当大于 8 时,则采用公式:3 + [ 5 * CPU_count ] / 8。

  -XX:MaxGCPauseMillis设置最大停止时间。(STW),单位是 ms。

  为了尽可能地把停顿时间控制在参数设置以内,收集器在工作时会调整堆大小或者其他一些参数。

  对于用户来讲,停顿时间越短越好,但是在服务器,我们注重并发量,整体的吞吐量,所以服务器端适合 Parallel。

  -XX:GCTimeRatio设置垃圾收集时间占总时间的比例,用于衡量吞吐量的大小。取值范围是(0,100),默认值99,也就是垃圾回收时间不超过 1%。与前面设置暂停时间的参数有一定的矛盾,暂停时间越长,本参数就容易超过设置的比例。

6、CMS GC:低延迟

6.1 简介

  在 JDK1.5 时期,HotSpot 推出了一款强交互应用中几乎可以认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,是第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程和用户线程同时工作。

  CMS GC 的关注点是尽可能缩减垃圾收集时用户线程的停顿时间,即缩小 STW。延迟越低,交互效果越好。

  目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS GC 就符合这个需求。

  从名称可以看出 CMS GC 采用的是标记-清除算法,也会存在 STW。

6.2 工作方式

在这里插入图片描述
  CMS GC 的过程分为四个主要的阶段:
  1、初始标记:STW,仅标记处 GC Roots 能够直接关联到的对象;
  2、并发标记:和用户线程一起进行,从 GC Roots 能够直接关联的对象开始遍历整个对象图的过程,耗时较长,但不会出现 STW;
  3、重新标记:STW,修正并发标记期间因为用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,即有些对象在并发标记时标记为垃圾,后来又复活了,就把它标记为不是垃圾;
  4、并发清除:和用户线程一起进行,清除重新标记后判断依据死亡的对象。

  尽管 CMS 是并发式的,即可以和用户线程一起运行,但是在初始标记和重新标记期间,还是会 STW,不过这两步耗时特别短,耗时最长的并发标记和并发清除都不需要停止用户线程,使得整体的回收是低延迟的。

  另外,因为用户线程没有中断,所以在 CMS 回收过程中,要保证用户线程有足够的内存可用,尽量不要出现“CMS 还在工作,内存又爆了,必须执行 Full GC”的局面。因此,CMS 的触发不像其他收集器那样等到老年代几乎被填满了才进行收集,而是当内存使用率达到一定阈值的时候,就开始进行了。

  当出现“CMS 还在工作,内存又爆了,必须执行 Full GC”的时候,会出现“Concurrent Mode Failure”报错,这时就会启动备用方案,临时启用 Serial Old GC 来重新进行老年代的垃圾收集,这时程序的停顿就会很长了。

  CMS 收集后会产生内存碎片,当进行新的对象分配内存,就不能使用指针碰撞了,只能选择空闲列表来执行内存分配了。

  因此,CMS 的优缺点很明显:

  优点:并发收集、低延迟;
  缺点:产生内存水平、对 CPU 资源非常敏感(因为并发会和用户线程抢占资源)、无法处理浮动垃圾。

  重新标记的范围是对并发标记过程中标注为垃圾的对象进行修正,看是不是哪些垃圾又复活了,但是,如果有一个对象,在并发标记阶段从不是垃圾变为垃圾,CMS 是没办法识别到的,只能留给下一次 CMS 进行处理,这种垃圾就叫做浮动垃圾。

  在 JDK9 中,CMS 被标记为 Deprecate 了,在 JDK14 中彻底删除了 CMS。

6.3 可设置参数

  -XX:+UseConcMarkSweepGC设置使用 CMS 收集器执行回收任务。设置后,会自动打开-XX:+UseParNewGC,即自动采用 ParNew GC + CMS GC 的组合。

  -XX:CMSInitiatingOccupanyFraction设置堆内存使用率的阈值,到了该阈值就启动 CMS。

  在 JDK5 以前,这个阈值默认是 68%,到了 JDK6 以后,默认值为 92%。

  如果内存使用增长缓慢,可以设置一个稍大的值,反之,内存使用增长较快,可以设置一个较小的值。设置较大的值可以避免频繁 GC,设置较少的值可以避免触发 Full GC,就是那个 Serial Old GC。
在这里插入图片描述
  -XX:+UseCMSCompactAtFullCollection用于指定在执行完 Full GC 后堆内存空间进行压缩整理,以此避免内存碎片的产生,不过由于内存压缩整理过程无法并发执行,所带来的的问题就是停顿时间变得更长了。

  -XX:CMSFullGCsBeforeCompaction设置在执行多少次 Full GC 后对内存进行压缩整理。

  -XX:ParallelCMSThreads设置 CMS 的线程数量。

  CMS 默认启动的线程数是:(ParallelCMSThreads + 3)/ 4。

  ParallelCMSThreads 是年轻代并行收集器的线程数,当 CPU 资源紧张的时候,受到 CMS GC 线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

7、Garbage First GC

7.1 简介

  Garbage First GC 简称 G1。
在这里插入图片描述
  它是在 Java7 update 4 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一,为了适应不断增大的内存和不断增大的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。

  官方对 G1 的设定目标是:在延迟可控的情况下获得尽可能高的吞吐量。

  有这样的使命,所以才担当起“全功能收集器”的重任与期望。

  使用 G1 收集器时,它将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32MB 之间,且为 2 的 N 次幂,即 1MB、2MB、4MB、8MB、16MB、32MB。可以通过参数-XX:G1HeapRegionSize设定。所有的 Region 大小相同,且在 JVM 生命周期内不会被改变。
在这里插入图片描述
  G1 是并行回收器,它把堆内存分割为很多不想管的区域(Region),物理上是不连续的。使用不同的 Region 来表示 Eden、Survivor0、Survivor1、老年代等。但是一个区域只能是其中的一种。图中蓝色的 H 表示Humonogous 区域,用来存储大对象,如果超过 1.5 个 Region ,就放到 H。

  G1 避免了在整个 Java 堆中进行全区域的垃圾收集,它跟踪每一个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据运行的手机时间,优先回收价值最大的 Region。

  比如说,现在有 100 个 Region,垃圾数量分别是 1、2、…、100,清理的时间消耗分别是100、99、…、1,很明显,第 100 个 Region 是价值最好的,因为清理他只要 1 个单位的时间,并且清除掉 100 个单位的垃圾。

  这种方式的侧重点在于回收垃圾最大量的区间(Region),所以 G1 的名字叫做:Garbage First。(垃圾量优先)。

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

  在 JDK7 正式启用 G1,称为 JDK9 以后的默认垃圾收集器,取代了 CMS 和 Parallel GC + Parallel Old GC 的组合。被 Oracle 官方称为“全功能的垃圾收集器”。
在这里插入图片描述
  CMS 在 JDK9 中标记为废弃。G1 在 JDK8 中还不是默认的垃圾收集器,需要使用-XX:+UseG1GC来启用。

7.2 工作方式

7.2.1 回收过程简介

  G1 GC 的垃圾回收过程主要包括以下三个环节:
  1、年轻代 GC(Young GC);
  2、老年代并发标记过程(Concurrent Marking);
  3、混合回收(Mixed GC)。
  如果需要,单线程、独占式、高强度的 Full GC 还是继续存在的。它针对 GC 的评估失败提供了一种失败保护机制,即强力回收。
在这里插入图片描述
  顺时针,Young GC -》Young GC + Concurrent Mark -》Mixed GC 的顺序进行垃圾回收。

  Remenbered Set 是解决对象的跨区域引用的问题。

  一个 Region 中的对象可能被其他 Region 中的对象所引用,那么,判断对象存活时,是否需要扫描整个 Java 堆才能保证准确呢?

  每一个 Region 都维护一个 Remenbered Set,每一次 Reference 类型数据写操作时,会产生一个 Write Barrier 暂时中断操作,然后检查将要写入的引用指向的对象是否和该 Reference 类型不在同一个 Region,如果不同,通过 CardTable 把相关引用信息记录到引用指向对象的所在 Region 对应的 Remenbered Set 中。
在这里插入图片描述
  简单来说,就是有一个 A 对象在年轻代,然后又一个 B 对象是在老年代的 Region 中,这个 B 引用了 A,就会在 A 对象所在 Region 中的 Remenbered Set 记录下这个 B的信息。当要清除这个 A 对象的时候,直接通过 Remenbered Set 查看引用 A 的且不再同一个 Region 区域的对象信息,避免了扫描整个堆区来找谁引用了 A。

  当进行垃圾收集时,在 GC 根结点的枚举范围加入到 Remenbered Set,就可以保证不进行全局扫描,也不会有遗漏。

7.2.2 Young GC

  当年轻代 Eden 区域用尽时,开始年轻代的回收。G1 的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期间,G1 GC 暂停所有的用户线程,启动多线程执行年轻代回收。然后从年轻代区间移动对象到 Survivor 区间或者老年代区间,也有可能是两个区间都有涉及。如下图所示。
在这里插入图片描述
  年轻代 GC 有以下几个过程:

  1、扫描根。根是指 static 变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同 RSet 记录的外部引用作为扫描存活对象的入口;

  2、更新 RSet。处理 dirty card queue 中的 card,更新 RSet。此阶段完成后,RSet 可以准确的反映老年代对所在内存分段中对象的引用;

  3、处理 RSet。识别被老年代对象指向的 Eden 中的对象,这些对象被认为是存活的对象。

  4、复制对象。存活的对象复制到 survivor 区,如果不够,则直接晋升到老年代。

  5、处理引用。处理 Soft、Weak、Phantom、Final、JNI Weak 等引用。最终 Eden 空间的数据为空,GC 停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

7.2.3 并发标记

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

  举个例子:一个 Web 服务器,Java 进程最大堆内存为 4GB,每 45 秒回新分配大约 2GB 内存给对象。那么,G1 会每 45 秒进行一次年轻代回收,每 31 小时整个堆的使用率会达到 45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

  并发标记分为以下几个过程:

  1、初始标记阶段。标记从根结点直接可达的对象。这个阶段是 STW 的,会触发一次 Young GC;

  2、根区域扫描。G1 GC 扫描 Survivor 区域直接可达的老年代区域对象,并标记被引用的对象。这个过程必须在 Young GC 之前完成;

  3、并发标记。和应用程序并发执行,此过程可能被 Young GC 中断。若发现区域中的对象都是垃圾,那么这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(存活对象的比例);

  4、在此标记。由于应用程序持续进行,需要修正上一次的标记结果。是 STW 的,G1 中采用了比 CMS 更快的初始快照算法:snapshot-at-the-beginning(STAB);

  5、独占清理。计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺垫,是 STW 的。这个阶段并不会实际上去做垃圾的收集;

  6、并发清理阶段。识别并清理完全空闲的区域。

7.2.4 混合回收

  当原来越多的对象晋升到老年代 Region 区域是,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC。该算法并不是一个 Old GC,除了回收整个 Young Region,还会回收一部分的 Old Region。需要注意的是,是一部分老年代,不是所有的老年代。从而可以对垃圾回收的时间进行控制,也要注意 Mixed GC 不是 Full GC。
在这里插入图片描述
  并发标记结束以后,老年代中百分百为垃圾的内存分的被回收了,部分为垃圾的内存分段被计算出来了。默认情况下,这些老年代的内存分段会分 8 次被回收。可以通过参数-XX:G1MixedGCCountTarget设置多少次。

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

  由于老年代中的内存分段默认分 8 次回收,G1 会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent参数可以设置这个阈值,默认为 65%,意思是一个 Region 中,垃圾占比达到 65% 就会被回收。因为如果垃圾占比太低,意味着存活对象占比高,在复制的时候回花费更多的时间。

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

7.2.5 Full GC(可选)

  G1 的初衷是避免 Full GC,但是如果上面方式不能正常工作,G1 会停止用户线程,使用单线程的内存回收算法进行垃圾回收,性能会非常差,STW 会很长。

  当堆内存太小,G1 在复制对象的时候没有空的内存分段可以,会进行 Full GC,这种情况可以通过增大内存解决。

  导致 Full GC 还有两种可能:
  1、Evacuation 的时候没有足够的 to-space 来存放晋升的对象;
  2、并发处理过程完成之前空间耗尽;

7.3 优缺点

  G1 GC 的优点有以下几个:

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

  2、分代收集
  从分代上看,G1 扔属于分代型垃圾收集器,它会区分年轻代和老年代,年轻代依然用 Eden 区和幸存者区。但从堆结构上看,它不要求整个 Eden 区、幸存者区或者老年代是连续的区域,也不再坚持固定大小和固定数量。
  把堆空间分成若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
  和之前的各类回收器不同,它同时兼顾年轻代和老年代,对比其他回收器,或者工作在年轻代,或者工作在老年代。
在这里插入图片描述
  3、可预测的停顿时间模型
  这是 G1 相对于 CMS 的另一大优势,G1 除了追求停顿外,还建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。这个通过对 Region 的价值估计来做到。

  G1 回收器的缺点:

  相对于 CMS,G1 还不具备全方位、压倒性的优势,比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 高。

  从经验上来说,在小内存应用上,CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势,平衡点在 6-8 GB 之间。

7.4 可设置参数

  -XX:+UseG1GC指定 G1 作为垃圾收集器。

  -XX:G1HeapRegionSize设置每个 Region 的大小,值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 。

  -XX:MaxGCPauseMillis设置期望达到的最大 GC 停顿时间指标(JVM 会尽力实现),默认值是 200 ms。

  -XX:ParallelGCThread设置 STW 工作线程数的值,最多为 8。

  -XX:ConcGCThreads设置并发标记的线程数,将 n 设置为并行垃圾回收线程数的 1/4 左右。

  -XX:InitiatingHeapOccupancyPercent设置触发并发 GC 周期的 Java 堆占用率阈值,超过这个值就触发 GC,默认值是 45。

  G1 的设计原则是简化 JVM 性能调优,开发人员只需要简单的三步即可完成调优:
  1、开启 G1 垃圾收集器;
  2、设置堆的最大内存;
  3、设置最大的停顿时间。

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

7.5 适用场景

  1、G1 是面向服务端应用,针对具有大内存、多处理器的场景。

  2、最主要的应用是需要低 GC 延迟,并具有大堆的应用程序提供解决方案,如在堆大小为 6 GB 或更大时,可预测的暂停时间可以低于 0.5 秒。

  3、替换 JDK1.5 中的 CMS 收集器,在下面的场景下,使用 G1 可能比 CMS 好:超过 50% 的堆被活动数据占用;对象分配频率或年代提升频率变化很大;GC 停顿时间过长。

  HotSpot 垃圾收集器里,除了 G1 以外,其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 可以采用应用线程承担后台的 GC 工作,即当 JVM 的 GC 线程处理速度慢时,系统会调用应用线程帮助加速垃圾回收过程。

7.6 G1 回收器优化建议

  年轻代大小:
  1、避免使用-Xmn-XX:NewRatio等相关选项显式设置年轻代大小;
  2、固定年轻代的大小会覆盖暂停时间目标;

  暂停时间目标不要太过严苛:
  1、G1 GC 的吞吐量目标是 90% 应用程序时间和 10% 垃圾回收时间;
  2、评估吞吐量时,暂停时间不要太严苛,太严苛会导致过多的垃圾回收开销,反过来直接影响吞吐量。

8、垃圾回收器总结

  截止 JDK8 ,一共有 7 款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
在这里插入图片描述
  怎么选择垃圾收集器?
  1、优先调整堆的大小让 JVM 自适应完成;
  2、如果内存小于 100M,使用串行收集器;
  3、如果是单核、单机程序,并且没有停顿时间的要求,串行收集器;
  4、如果是多 CPU、需要高吞吐量、运行停顿时间超过 1 秒,选择并行或者 JVM 自己选择;
  5、如果是多 CPU、追求停顿时间,需要快速响应,使用并发收集器;

  官方推荐 G1,性能高。现在的互联网的项目,基本上都是使用 G1。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值