垃圾收集器

文章目录

  • 垃圾收集器
  • 一、概述
    • 1、垃圾回收器概述
    • 2、垃圾收集器分类
    • 3、GC 的性能指标
    • 4、垃圾回收器发展史
    • 5、7 种经典的垃圾收集器
    • 6、查看默认的垃圾收集器
  • 二、Serial 收集器:串行回收
    • 1、概述
    • 2、Serial Old 收集器
    • 3、分代组合
    • 4、优缺点
    • 5、使用场景
    • 6、参数配置
  • 三、ParNew 收集器:并行回收
    • 1、概述
    • 2、分代组合
    • 3、优缺点
    • 4、参数配置
  • 四、Parallel 收集器:吞吐量优先
    • 1、概述
    • 2、Parallel Old 收集器
    • 3、分代组合(jdk8默认组合)
    • 4、使用场景
    • 5、参数配置
  • 五、CMS 收集器:低延迟
    • 1、概述
    • 2、分代组合
    • 3、回收过程
    • 4、内存的要求
    • 5、优缺点
    • 6、为什么不用标记-整理
    • 7、使用场景
    • 8、参数配置
  • 六、G1 收集器:区域划分代式
    • 1、概述
    • 2、分区 Region:化整为零
    • 3、Remembered Set
    • 4、G1 回收过程
      • 1)年轻代回收(Minor GC)
      • 2)并发标记(Concurrent Mark)
      • 3)混合回收(Mixed GC)
      • 4)Full GC(可能发生)
    • 5、特点(优势)
    • 6、缺点
    • 7、适用场景
    • 8、参数配置
    • 9、G1收集器的使用
  • 七、ZGC 收集器:低延迟
  • 八、总结

垃圾收集器

一、概述

1、垃圾回收器概述

垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的 JVM 来实现。

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

没有什么垃圾收集器是完美的,我们只能根据具体使用场景来选择最合适的收集器。

2、垃圾收集器分类

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

  • 独占式:STW一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
  • 并发式:STW与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。

线程数分,可以分为串行垃圾回收器并行垃圾回收器

  • 串行:同一时间段内只有一个 CPU 用于执行垃圾回收操作
  • 并行:并行收集可以运用多个 CPU 同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-World”机制。

3、GC 的性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • 内存占用:Java 堆区所占的内存大小。

吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。现在标准:在最大吞吐量优先的情况下,降低停顿时间

吞吐量优先,意味着 STW 的总时间要尽可能短:0.2 + 0.2 = 0.4 s(但是每次STW的时间长)

垃圾收集器_第1张图片

暂停时间优先,意味着单次 STW 的时间尽可能短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5 s(但是存在线程切换,STW的总时间长)

垃圾收集器_第2张图片

4、垃圾回收器发展史

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

  • 1999 年随 JDK1.3.1 一起来的是串行方式的 Serial GC,它是第一款 GC。ParNew GC是 Serial GC的多线程版本

  • 2002 年 2 月 26 日,Parallel GC 和 CMS(Concurrent Mark Sweep) GC 跟随 JDK1.4.2 一起发布·

  • Parallel GC 在 JDK6 之后成为 HotSpot 默认 GC。

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

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

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

  • 2018 年 9 月,JDK11 发布。引入 Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。

    同时,引入 ZGC:可伸缩的低延迟垃圾回收器(Experimental)

  • 2019 年 3 月,JDK12 发布。增强 G1,自动返回未用堆内存给操作系统。

    同时,引入 Shenandoah GC:低停顿时间的 GC(Experimental)。

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

  • 2020 年 3 月,JDK14 发布。删除 CMS 垃圾回收器。扩展 ZGC 在 macos 和 Windows 上的应用

5、7 种经典的垃圾收集器

按照回收方式分:

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel old
  • 并发回收器:CMS、G1

按照分代关系分:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge

  • 老年代收集器:Serial Old、Parallel Old、CMS

  • 整堆收集器:G1

垃圾收集器_第3张图片

  • 两个收集器间有连线,表明它们可以搭配使用

    其中 Serial Old 作为 CMS 出现 “Concurrent Mode Failure” 的后备预案。

  • 红色虚线

    JDK 8 中:弃用 Serial+CMS、ParNew+Serial Old 这两个组合,并在 JDK9 中完全取消了这些组合的支持,即:移除。

  • 绿色虚线

    JDK14 中:弃用 Parallel Scavenge 和 Serialold GC 组合(JEP366)

  • JDK14 中:删除 CMS 垃圾回收器(JEP363)

6、查看默认的垃圾收集器

-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)

使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

二、Serial 收集器:串行回收

1、概述

  • Serial 收集器是最基本、历史最悠久的垃圾收集器。JDK1.3 之前回收新生代唯一的选择。
  • Serial 收集器是 HotSpot虚拟机Client模式 默认的 年轻代 垃圾收集器
  • Serial 收集器采用复制算法串行回收stop-the-world机制 执行内存回收。
  • Serial 收集器进行垃圾收集时,**必须暂停其他所有的工作线程,**直到它收集结束。

2、Serial Old 收集器

除了年轻代之外,Serial 收集器还提供用于执行老年代垃圾收集的 Serial Old 收集器。

  • Serial Old 收集器是 Client模式 默认的 老年代 垃圾回收器。
  • Serial Old 收集器在 Server模式 主要有两个用途:
    1. 与新生代的 Parallel Scavenge 收集器配合使用
    2. 作为老年代 CMS 收集器的后备垃圾收集方案

3、分代组合

  • 年轻代:Serial 收集器。串行回收复制算法(HotSpot 中 Client 模式下 默认的 新生代 垃圾收集器)
  • 老年代:Serial Old 收集器。串行回收标记-整理算法(Client 模式下 默认的 老年代 垃圾收集器)

垃圾收集器_第4张图片

4、优缺点

优点:对于 Client 模式 单核CPU 的场景,由于没有线程交互的开销,可以获得最高的收集效率。简单而高效。

缺点:

  • 只会使用 一个 CPU 或 一条收集线程 去完成垃圾收集工作。
  • 进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)

5、使用场景

Serial 收集器适用于:

  • Client 模式 单核CPU。

  • GC耗时较少,且不是很频繁的场景,使用串行回收器也是可以接受的。

    例如用户的桌面应用,可用内存一般不大(一两百M),可以在较短时间内完成垃圾收集(一百多ms以内)

而对于交互较强的应用而言,就不适合使用串行垃圾收集器了。所以 Java Web 中是不会采用串行垃圾收集器的。

6、参数配置

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

等价于年轻代用 Serial 收集器,且老年代用 Serial Old 收集器

三、ParNew 收集器:并行回收

1、概述

  • ParNew 收集器是 Serial 收集器的多线程版本。(Par:Parallel 的缩写,New:只能处理新生代)
  • ParNew 收集器是 很多JVM 在 Server模式 默认的 年轻代 垃圾收集器
  • ParNew 收集器采用复制算法并行回收stop-the-world机制 执行内存回收。
  • 除 Serial 收集器外,目前只有 ParNew 收集器 能与 CMS 收集器配合工作。

2、分代组合

  • 年轻代:ParNew 收集器,并行回收复制算法(很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器)
  • 老年代:Serial Old 收集器,串行回收标记-整理算法 (Client 模式下默认的老年代垃圾回收器)

垃圾收集器_第5张图片

  • 对于年轻代,回收次数频繁,使用并行方式高效。
  • 对于老年代,回收次数少,使用串行方式节省资源。(CPU 并行需要切换线程,串行可以省去切换线程的资源)

3、优缺点

优点:多核CPU的场景下,可以充分利用多 CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。

缺点:单核CPU的情况下,ParNew 收集器并不比Serial 收集器更高效。

4、参数配置

-XX:+UseParNewGC:手动指定 年轻代 使用 ParNew 收集器,不影响老年代。

-XX:ParallelGCThreads:限制线程数量,默认开启和 CPU 数相同的线程数。

四、Parallel 收集器:吞吐量优先

1、概述

  • 与 ParNew 收集器相同的是:
    • Parallel Scavenge 收集器也采用复制算法并行回收stop-the-world机制 执行内存回收。
  • 与 ParNew 收集器不同的是:
    • Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量,吞吐量优先。
    • Parallel Scavenge 收集器支持 自适应调节策略。在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。

2、Parallel Old 收集器

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

  • Parallel Old 收集器采用标记-整理算法并行回收stop-the-world机制 执行内存回收。

3、分代组合(jdk8默认组合)

  • 年轻代:Parallel Scavenge 收集器,并行回收复制算法
  • 老年代:Parallel Old 收集器,并行回收标记-整理算法

垃圾收集器_第6张图片

在吞吐量优先的场景中,Parallel Scavenge 收集器 和 Parallel Old 收集器的组合,在 Server 模式下的内存回收性能很不错。

4、使用场景

Parallel Scavenge 收集器 适用于:

  • 追求高吞吐(高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务)
  • 在后台运算而不需要太多交互(高吞吐情况下,STW的总时间短,但是单次STW的时间长,可能影响用户体验)

例如:那些执行批量处理、订单处理、工资支付、科学计算的应用程序

5、参数配置

-XX:+UseParallelGC :手动指定 年轻代 使用 Parallel Scavenge 收集器。

-XX:+UseParallelOldGC :手动指定 老年代 使用 Parallel Old 收集器。

  • 上面两个参数,默认 jdk8 是开启的。默认开启一个,另一个也会被开启。(互相激活)

-XX:ParallelGCThreads :设置 Parallel Scavenge 收集器的线程数。

  • P a r a l l e l G C T h r e a d s = { C P U _ C o u n t ( C P U _ C o u n t < = 8 ) 3 + ( 5 ∗ C P U _ C o u n t / 8 ) ( C P U _ C o u n t > 8 ) ParallelGCThreads = \begin{cases} CPU_Count & \text (CPU_Count <= 8) \\ 3 + (5 * CPU_Count / 8) & \text (CPU_Count > 8) \end{cases} ParallelGCThreads={CPU_Count3+(5CPU_Count/8)(CPU_Count<=8)(CPU_Count>8)

-XX:MaxGCPauseMillis :设置垃圾收集器最大的停顿毫秒数(即 STW 的时间)

  • 为了尽可能地保证内存回收花费时间不超过设定值,JVM在工作时会调整 Java 堆大小和其他与GC相关的参数。

  • 默认情况下,JVM没有设置该值,GC的暂停时间主要取决于堆中实时的数据量。

  • 该参数应谨慎使用。太小的值将导致系统花费过多的时间进行垃圾回收。

    原因是为满足最大暂停时间,VM将设置更小的堆,以存储相对少量的对象,来提升回收速率,会导致更高频率的GC。

-XX:GCTimeRatio=N :GC花费时间不超过执行时间的 1 / N+1,N的取值范围是(0, 100),用于衡量吞吐量的大小。

  • 换句话说,此参数的值表示 运行用户代码时间 是 GC运行时间 的 N 倍。

    默认值为99,即 运行用户代码时间 是 GC停顿时间 的 99倍,即GC最大花费时间比率为1%

  • 举个官方的例子,参数设置为19,那么GC最大花费时间的比率=1/(1+19)=5%,

    程序每运行100分钟,允许GC停顿共5分钟,其吞吐量=1-GC最大花费时间比率=95%

  • 与上一个参数MaxGCPauseMillis 有一定矛盾性。暂停时间越长,GCTimeRatio参数就容易超过设定的比例。

-XX:+UseAdaptivesizePolicy :开启 Parallel Scavenge 收集器 的 自适应调节策略

  • 在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
  • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

五、CMS 收集器:低延迟

1、概述

  • CMS(Concurrent-Mark-Sweep)收集器是 HotSpot 虚拟机中第一款真正意义上的并发收集器,

    它第一次实现了 垃圾收集线程 与 用户线程 同时工作。

  • CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。

    停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

  • CMS 收集器采用标记-清除算法并发回收 ,并且也会 stop-the-world

  • JDK9 中,将 CMS 收集器 标记为 Deprecate ;JDK14 中,删除了CMS 收集器

在 G1 出现之前,CMS 使用还是非常广泛的。一直到今天,仍然有很多系统使用 CMS GC。

2、分代组合

CMS 作为老年代的收集器,新生代只能选择 ParNew 或者 Serial 收集器中的一个。不能和 Parallel Scavenge 收集器 配合使用。

3、回收过程

垃圾收集器_第7张图片

CMS 收集器回收的整个过程分为 4 个主要阶段:

  1. 初始标记(Initial-Mark)

    • 主要任务:标记出 GCRoots 能直接关联到的对象。(由于直接关联对象比较少,所以这里的速度非常快
    • 在这个阶段中,程序中所有的用户线程都会发生STW,一旦标记完成,就会恢复之前被暂停的所有用户线程。
  2. 并发标记(Concurrent-Mark)

    • 主要任务:从 GC Roots 的直接关联对象开始,遍历整个对象图进行标记。
    • 耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  3. 重新标记(Remark)

    • 主要任务:修正「并发标记」期间,因用户线程并发执行而导致的,标记产生变动的那一部分对象的标记记录。

      注意:这里修正的是已标记的。也就是说,只会将 已标记 修正为 未标记,不会将 未标记 修正为 已标记

    • 这个阶段的停顿时间 通常会比 「初始标记阶段」稍长一些,但也远比「并发标记阶段」的时间短。

  4. 并发清除(Concurrent-Sweep)

    • 主要任务:清理掉标记为已经死亡的对象,释放内存空间。
    • 由于不需要移动存活对象,所以这个阶段也是可以与用户线程并发执行的。

尽管 CMS 收集器采用的是并发回收,但是在「初始标记」和「重新标记」阶段仍然需要STW暂停程序中的工作线程,不过暂停时间并不会太长。由于最耗时的「并发标记」与「并发清除」阶段都不需要暂停工作,所以整体的回收是低停顿的

4、内存的要求

由于在 CMS 回收过程中用户线程没有中断,所以 CMS 收集器不能像其他收集器那样,等到老年代几乎完全被填满了再进行GC,而是
在堆内存使用率达到某一阈值时,便开始进行回收,以确保在 CMS 工作过程中依然有足够的空间支持应用程序运行。

要是在 CMS 运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

5、优缺点

优点:并发收集,效率高,延迟低,响应快

缺点:

  1. 会产生内存碎片。

    导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发 FullGC。

  2. 总吞吐量会降低。

    在并发阶段,虽然不会导致用户停顿,但会因为占用一部分线程而导致应用程序变慢,总吞吐量会降低。

  3. 无法处理浮动垃圾。

    在并发标记阶段,由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,因此,在并发标记阶段如果产生新的垃圾对象,CMS 将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收。

6、为什么不用标记-整理

标记-清除会产生内存碎片,为什么不使用 标记-整理?

因为在「并发清除」阶段,如果进行内存的整理,用户线程使用的内存就不能并发执行了。要保证用户线程能并发执行,前提是它运行的资源不受影响。因此,标记-整理 更适合 “Stop the World” 的场景下使用。

7、使用场景

CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

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

8、参数配置

-XX:+UseConcMarkSweepGC :手动指定老年代使用 CMS 收集器。

  • 开启该参数后,会自动开启-xx:+UseParNewGC。即年轻代用ParNew,老年代用CMS,老年代兜底用Serial Old

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

  • 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。
  • 当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

-XX:CMSInitiatingOccupanyFraction :设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。

  • JDK5 及以前,默认值为 68,即当老年代的空间使用率达到 68%时,会执行一次 CMS 回收;

    JDK6 及之后,默认值为 92,即当老年代的空间使用率达到 92%时,会执行一次 CMS 回收;

  • 如果内存增长缓慢,可以设置较大的阀值,降低 CMS 的触发频率,减少老年代回收的次数,改善应用程序性能;

    如果内存增长很快,应该降低这个阈值,避免频繁触发 Serial Old 收集器兜底,降低 Full GC 的执行次数。

-XX:+UseCMSCompactAtFullCollection :指定在执行完 Full GC 后对内存空间进行压缩整理

  • 可以一定程度避免内存碎片的产生。
  • 不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。

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

六、G1 收集器:区域划分代式

1、概述

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

为什么名字叫 Garbage First 呢?

因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者 1 区,老年代等。

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

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

垃圾收集器_第8张图片

2、分区 Region:化整为零

G1 收集器将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32MB 之间,且为 2 的 N 次幂(1、2、4、8)。所有的 Region 大小相同,且在 JVM 生命周期内不会被改变。

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

垃圾收集器_第9张图片

G1 划分了一个 Humongous 区(上图的H),主要用于存储大对象。

  • 如果一个对象的大小超过 1.5 个 Region,就放到 H。
  • 如果一个 H 区装不下一个大对象,那么 G1 会寻找连续的 H 区来存储。
  • 为了能找到连续的 H 区,有时候不得不启动 Full GC。
  • G1 的大多数行为都把 H 区作为老年代的一部分来看待。

每个 Region 都是通过指针碰撞来分配空间

  • 所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器。
  • 分配内存就是把指针移动一段与对象大小相等的距离。

G1 收集器在后台维护了一个优先列表,每次 GC 根据允许的收集时间,优先选择回收价值最大的 Region。这种使用 Region 划分内存空间以及有优先级的 Region 回收方式,保证了 G1 收集器在有限时间内尽可能高的收集效率(把内存化整为零)。

3、Remembered Set

问题:一个对象被不同区域引用

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

方案:无论 G1 还是其他分代收集器,JVM 都是使用 Remembered Set 来避免全局扫描:

  • 每个 Region 都有一个对应的 Remembered Set

  • 每次 Reference 类型数据写操作时,都会产生一个 写屏障(Write Barrier)暂时中断操作;

  • G1 收集器:检查 将要写入的引用 指向的对象 是否和该 Reference 类型数据在不同的 Region

    其他收集器:检查老年代对象是否引用了新生代对象

  • 如果不同,通过 卡表(Card Table)把 相关引用信息 记录到引用指向对象的所在 Region 对应的 Remembered Set 中;

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

垃圾收集器_第10张图片

4、G1 回收过程

G1 收集器的垃圾回收过程主要包括如下三个环节:

按 Young GC -> Young GC + Concurrent Mark -> Mixed GC 的顺序,进行垃圾回收。(可能会发生Full GC)

垃圾收集器_第11张图片

举个例子:

一个 Web 服务器,Java 进程最大堆内存为 4G,每分钟响应 1500 个请求,每 45 秒钟会新分配大约 2G 的内存。G1 会每 45 秒钟进行一次年轻代回收,每 31 个小时整个堆的使用率会达到 45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

1)年轻代回收(Minor GC)

G1 的年轻代 GC 是一个并行的独占式收集器。在年轻代 GC,G1 GC 暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到 Survivor 区间 或 Old区间,也有可能是两个区间都会涉及。

  • JVM 启动时,G1 先准备好 Eden 区,程序在运行过程中不断创建对象到 Eden 区。

  • 当 Eden 空间耗尽时,G1 会触发一次 年轻代垃圾回收(只会回收 Eden 区和 Survivor 区)

  • 首先 G1 停止应用程序的执行(Stop-The-World),G1 创建回收集(Collection Set)

    回收集是指需要被回收的内存分段的集合,年轻代垃圾回收的回收集包括 Eden区 和 Survivor区 所有的内存分段。

垃圾收集器_第12张图片

然后开始如下回收过程:

  1. 扫描根

    GC Roots 连同 Remembered Set 记录的外部引用,作为扫描存活对象的入口。

  2. 更新 Remembered Set

    处理 Dirty Card Queue 中的 card(见备注),更新 RSet,

    此阶段完成后,RSet 可以准确的反映老年代对所在的内存分段中对象的引用。

  3. 处理 Remembered Set

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

  4. 遍历对象树,复制对象

    Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段;

    Survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加 1;

    如果达到阈值,会被复制到 Old 区中空的内存分段。

    如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间。

  5. 处理引用

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

Dirty Card Queue:

对于赋值语句 obj1.field=obj2,JVM会在之前和之后执行特殊的操作以在 Dirty Card Queue 保存对象引用信息的card。在年轻代回收的时候,G1会对 Dirty Card Queue 中所有的card进行处理 以 更新 RSet,保证 RSet 实时准确的反映引用关系。

为什么不在赋值语句处直接更新RSet?这是为了性能的需要。RSet的处理需要线程同步,开销会很大,使用队列保证性能。

2)并发标记(Concurrent Mark)

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

  1. 初始标记阶段(STW的)

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

  2. 根区域扫描(Root Region Scanning)

    G1 GC 扫描 Survivor 区直接可达的老年代区域对象,并标记被引用的对象。(这一过程必须在 YoungGC 之前完成)

  3. 并发标记(Concurrent Marking)

    在整个堆中进行并发标记(和应用程序并发执行),此过程可能被 YoungGC 中断。

    在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。(实时回收)

    同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

  4. 重新标记(Remark,STW 的)

    由于应用程序持续进行,需要修正上一次的标记结果。

    G1 中采用了比 CMS 更快的初始快照算法:snapshot-at-the-beginning(SATB)。

  5. 独占清理(cleanup,STW的)

    计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可以混合回收的区域。

    这个阶段并不会实际上去做垃圾的收集,为下阶段做铺垫。

  6. 并发清理

    识别并清理完全空闲的区域。

3)混合回收(Mixed GC)

当越来越多的对象晋升到 老年代 Old Region,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集,即 Mixed GC。

Mixed GC 除了回收整个 Young Region,还会回收一部分的 Old Region。这里需要注意:是一部分老年代,而不是全部老年代(G1收集器会根据允许的收集时间,优先选择回收价值最大的 Region进行回收),从而对垃圾回收的耗时进行控制。

也要注意的是 Mixed GC 并不是 Full GC。

垃圾收集器_第13张图片

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

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

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

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

4)Full GC(可能发生)

G1 的初衷就是要避免 Full GC 的出现。Full GC 会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

要避免 Full GC 的发生,一旦发生需要进行调整。

导致 G1 Full GC 的原因可能有两个:

  • 堆内存太小,G1 在复制存活对象的时候没有空的内存分段可用。(这种情况可以通过增大内存解决)
  • 并发处理过程完成之前空间耗尽。

5、特点(优势)

与其他 GC 收集器相比,G1 使用了全新的分区算法,其特点如下所示:

并行与并发

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

分代收集

  • 其他回收器,或者工作在年轻代,或者工作在老年代;它同时兼顾年轻代和老年代
  • 虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
    • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
    • 从堆的结构上看,它不要求整个 Eden 区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。

空间整合

  • G1 将内存划分为一个个的 Region,内存的回收是以 Region 作为基本单位的。
  • Region 之间是复制算法,但整体上实际可看作是标记-整理算法,两种算法都可以避免内存碎片。

可预测的停顿

这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

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

6、缺点

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

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

7、适用场景

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

最主要的应用是需要低 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 线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

8、参数配置

-XX:+UseG1GC:手动指定使用 G1 垃圾收集器执行内存回收任务(JDK1.8需要手动指定,JDK1.9以后默认)

-XX:G1HeapRegionSize :设置每个 Region 的大小。值是 2 的幂,范围是1~32MB,默认是堆内存的 1/2000。

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

-XX:ConcGCThreads :设置并发标记的线程数。

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

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

9、G1收集器的使用

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

  • 第一步:开启 G1 垃圾收集器(-XX:+UseG1GC
  • 第二步:设置堆的最大内存(-Xms-Xmx-XX:G1HeapRegionSize
  • 第三步:设置最大的停顿时间(-XX:MaxGCPauseMillis

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

七、ZGC 收集器:低延迟

ZGC 收集器 在尽可能不影响吞吐量的前提下,实现在任意堆内存大小下都可以把GC的停顿时间限制在10ms以内的低延迟。

《深入理解 Java 虚拟机》一书中这样定义 ZGC:ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

ZGC 的工作过程可以分为 4 个阶段:并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射等。

ZGC 几乎在所有地方并发执行,除了初始标记是 STW 的。所以停顿时间几乎就耗费在初始标记上,这部分的实际时间是非常少的。

八、总结

截止 JDK1.8,一共有 7 款不同的垃圾收集器,

  • Client模式下默认的垃圾回收器:年轻代 Serial + 老年代 Serial Old
  • JDK1.8默认的垃圾回收器:年轻代 Parallel Scavenge + 老年代 Parallel Old
  • JDK1.9默认的垃圾回收器:G1(Garbage First)

GC 发展阶段:Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC

每一款的垃圾收集器都有不同的特点:

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

在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器:

  • 单 CPU + Client 模式 或 内存小于100M,使用串行收集器

    年轻代 Serial + 老年代 Serial Old

  • 多 CPU、需要高吞吐量、对响应速度要求不是高:

    年轻代 Parallel Scavenge + 老年代 Parallel Old

  • 多 CPU、需要快速响应、对吞吐量要求不是很高:

    年轻代 ParNew + 老年代 CMS

  • 多 CPU、在延迟可控的情况下,获得尽可能高的吞吐量

    G1

最后需要明确一个观点:

  1. 没有最好的收集器,更没有万能的收集
  2. 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

你可能感兴趣的:(JVM,jvm,java)