JVM的垃圾收集器

JVM的垃圾收集器

   概要

   垃圾收集算法就像是 Java 中的接口一样,而垃圾收集器是接口的具体实现。所以,不同的厂商,不同版本的虚拟机实现的方式都有所不同。甚至是很大的差别。

   首先,了解下 HotSpot虚拟机中 9款垃圾回收器的发布时间及其对应的 JDK版本,如下图:

   接着,了解下 CMS和G1这两个垃圾回收器的生命线: 

   JDK 1.4.1 时 ,CMS 垃圾收集器被引入,在2020年3月,JDK 14 版本,CMS从 JDK中移除。 

   G1垃圾收集器 在 JDK 7 时引入,在 JDK 9 时G1取代 CMS 成为了默认的垃圾收集器。

   就目前来说,JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC,下面我们来看看这两大类的垃圾收集器。

   如下图:

   说明:分代垃圾收集器中,新生代有 Serial、ParNew、Parallel Scavenge,老年代包括 CMS、MSC、Parallel old,收集器之间的连线说明两者可以搭配使用。

   一、Serial 垃圾收集器

   Serial 是最基本,历史最悠久,也是最简单的一个收集器。它是一个单线程的收集器。

   1)新生代:使用 Serial GC,单线程执行,回收新生代中的垃圾。

   2)老年代:使用 Serial Old GC,Serial Old 收集器是 Serial 收集器的老年代版本,同样是单线程的,使用的是“标记-整理算法”。

   配置:-XX:+UseSerialGC

   适用场景:适用于单处理器的环境,或者内存较小、应用简单的场景。

   二、Parallel Scavenge 垃圾收集器

   1)新生代:使用 Parallel GC,多线程回收,称为 Parallel Scavenge 或 Minor GC,目标是通过多线程提高回收效率。

   2)老年代:使用 Parallel Old GC,多线程回收老年代的对象。同样,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法”。需要注意的是,如果老年代使用 Parallel Old 那么新生代就只能使用 Parallel Scavenge 与之配合。

   配置:

  • -XX:+UseParallelGC(开启新生代并行回收)
  • -XX:+UseParallelOldGC(开启老年代并行回收)

   适用场景:适用于高吞吐量的应用,如批处理和后台任务。

   三、CMS(Concurrent Mark-Sweep)垃圾收集器

   1.  基本介绍

   CMS垃圾收集器是一种以获取最短回收停顿时间为目标的收集器。它是第一个关注 GC 停顿时间(STW 的时间)的垃圾收集器。之前的垃圾收集器,要么是串行的垃圾回收方式,要么只关注系统吞吐量。它将用户体验放在第一位的角色,追求流畅的用户交互,避免因为垃圾收集导致的长时间停顿。

   补充一下:STW(Stop the World)指的是暂停应用程序的所有线程,以便垃圾回收器能够安全地访问和处理内存。这个阶段是为了确保在标记和清理对象时,没有其他线程在修改这些对象,防止出现不一致的状态。

   CMS启用参数:-XX:+UseConMarkSweepGC,CMS是老年代垃圾收集器,使用的是标记-清除算法。

   适用场景:对延迟敏感的场景,适合响应时间优先的应用(如 Web 应用服务器)。

   2.  三色标记算法

   CMS 垃圾收集器之所以能够实现对 GC 停顿时间的控制,其本质来源于对「可达性分析算法」的改进,即三色标记算法。在 CMS 出现之前,无论是 Serial 垃圾收集器,还是 ParNew 垃圾收集器,以及 Parallel Scavenge 垃圾收集器,它们在进行垃圾回收的时候都需要 STW,无法实现垃圾回收线程与用户线程的并发执行。

   在垃圾收集器中,主要采用三色标记算法来标记对象的可达性:  

  • 白色:表示对象尚未被访问。初始状态时,所有的对象都被标记为白色。
  • 灰色:表示对象已经被标记为存活,但其引用的对象还没有全部被扫描。灰色对象可能会引用白色对象。
  • 黑色:表示对象已经被标记为存活,并且该对象的所有引用都已经被扫描过。黑色对象不会引用任何白色对象。

   三色标记算法的工作流程大致如下:

   1)初始化时,所有对象都标记为白色。

   2)将所有的 GC Roots 对象标记为灰色,并放入灰色集合。

   3)从集合中选择一个灰色对象,将其标记为黑色,并将其引用的所有白色对象标记为灰色,然后放入灰色集合。

   4)重复步骤3,直到灰色集合为空。 

   5)最后,所有黑色对象都是活跃的,白色对象都是垃圾

   3.  CMS回收过程

   CMS的整个回收过程可以抽象成下图:

   CMS 垃圾收集器通过三色标记算法,实现了垃圾回收线程与用户线程的并发执行,从而极大地降低了系统响应时间,提高了强交互应用程序的体验。

   它的运行过程分为 4 个步骤,包括:

   1)初始标记 (CMS initial mark)

   指的是寻找所有被 GCRoots 引用的对象,该阶段需要 STW。仅仅只是标记一下GCRoots能直接关联到的对象,并不需要做整个引用的扫描,因此速度很快。

   说明:在此阶段,标记所有根对象(GCRoots)为灰色,并将其直接引用的对象标记为灰色。

   2并发标记 (CMS Concurrent mark)

   指的是对「初始标记阶段」标记的对象进行整个引用链的扫描,该阶段不需要 STW。 对整个引用链做扫描需要花费非常多的时间,因此通过垃圾回收线程与用户线程并发执行,可以降低垃圾回收的时间。这个阶段在整个过程中耗时最长

   这也是 CMS 能极大降低 GC 停顿时间的核心原因,但这也带来了一些问题,即:并发标记的时候,引用可能发生变化,因此可能发生漏标(本应该回收的垃圾没有被回收)和多标(本不应该回收的垃圾被回收)。

  说明:这个阶段,灰色对象会被处理,检查它们引用的对象,并将这些对象标记为灰色。这一过程会一直进行,直到没有灰色对象为止。

  3)重新标记 (CMS remark)

  为了修正「并发标记阶段」期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要STW。

  说明:这个阶段,垃圾收集器会遍历之前标记为灰色的对象,检查它们引用的新对象。这些新对象将被标记为灰色或黑色,表示它们是可达的。如果一个对象已经被标记为黑色,垃圾收集器会跳过对该对象的再次处理,避免多标的情况。

  4)并发清除 (CMS Concurrent Sweep)

  指的是将标记为垃圾的对象(白色的对象)进行清除,该阶段不需要STW。 在这个阶段,垃圾回收线程与用户线程可以并发执行,因此并不影响用户的响应时间。

   3. 优缺点

   优点:并发收集、低停顿。

   缺点: 处理器资源敏感、内存碎片、CMS 无法处理浮动垃圾,具体如下:

   1)处理器资源敏感

   CMS收集器是比较消耗CPU资源的,对处理器资源是比较敏感的。因此在 CPU 资源紧张的情况下,CMS 的性能会大打折扣。

   在并发阶段,它不会导致用户线程停顿,但会占用一部分线程(或者说处理器的计算能力)来进行垃圾回收,从而导致应用程序变慢,降低总吞吐量。低延迟和高吞吐,往往无法同时达成,低延迟有时是牺牲高吞吐换得的,有得必有失。 

   默认情况下,CMS 启用的垃圾回收线程数是(CPU数量 + 3)/4,当 CPU 数量很大时,启用的垃圾回收线程数占比就越小。但如果 CPU 数量很小,例如只有 2 个 CPU,垃圾回收线程占用就达到了 50%,这极大地降低系统的吞吐量,无法接受。

   CMS 回收线程数量可以通过-XX:ParallelCMSThreads=<N>个JVM参数来设定,其中<N>代表期望的线程数。

   注意:这个参数只影响CMS中进行并发标记和清除的线程数量,并不影响其他部分(如初始标记和重新标记)的线程数量。

   2)  内存碎片

   CMS 采用的是「标记-清除」算法,会产生大量的内存碎片,导致空间不连续,当出现大对象无法找到满足大小并且连续的内存空间时,就会触发下一次垃圾收集,这会导致系统的停顿时间变长。 

   3)  无法处理浮动垃圾

   当 CMS 在进行垃圾回收的时候,应用程序还在不断地产生垃圾,这些垃圾会在 CMS 垃圾回收结束之后产生,这些垃圾就是浮动垃圾,CMS 无法处理这些浮动垃圾,只能在下一次 GC 时清理掉。 

   四、G1(Garbage First)垃圾收集器 

   G1 垃圾收集器在 JDK 1.7 时引入,在 JDK 9 时G1 取代 CMS 成为了默认的垃圾收集器。启用参数:-XX:+UseG1GC

   G1 打破了传统的分代垃圾收集模式,将堆内存划分为多个Region,这些 Region 可以动态分配给新生代和老年代。G1 通过并发标记和回收来优先处理垃圾最多的区域,并能够同时处理新生代和老年代中的对象。G1 会在执行 Young GC 时专门处理新生代区域,在Mixed GC时同时回收老年代和新生代区域。

   G1 有五个属性:分代、增量、并行、标记整理、STW

   1.  分代

   之前提到过新生代和老年代。G1 也是基于这个思想进行设计的。它将堆内存分为多个大小相等的区域(Region),每个区域都可以是 Eden 区、Survivor 区或者 Old 区。

   如下图:

 


   可以通过 -XX:G1HeapRegionSize=n 来设置 Region 的大小,可以设定为 1M、2M、4M、8M、16M、32M(不能超过)。

   G1 有专门分配大对象的 Region 叫 Humongous 区,而不是让大对象直接进入老年代的 Region 中。在 G1 中,大对象的判定规则就是一个大对象超过了一个 Region 大小的 50%,比如每个 Region 是 2M,只要一个对象超过了 1M,就会被放入 Humongous 中,而且一个大对象如果太大,可能会横跨多个 Region 来存放。

   G1 会根据各个区域的垃圾回收情况来决定下一次垃圾回收的区域,这样就避免了对整个堆内存进行垃圾回收,从而降低了垃圾回收的时间。 

   2. 增量

   G1 可以以增量方式执行垃圾回收,这意味着它不需要一次性回收整个堆空间,而是可以逐步、增量地清理。有助于控制停顿时间,尤其是在处理大型堆时。

   3. 并行

   G1 垃圾回收器可以并行回收垃圾,这意味着它可以利用多个 CPU 来加速垃圾回收的速度,这一特性在年轻代的垃圾回收(Minor GC)中特别明显,因为年轻代的回收通常涉及较多的对象和较高的回收速率。 

   4. 标记整理

   在进行老年代的垃圾回收时,G1 使用标记-整理算法。这个过程分为两个阶段:标记存活的对象和整理(压缩)堆空间。通过整理,G1 能够避免内存碎片化,提高内存利用率。

   新生代的垃圾回收(Minor GC)使用复制算法,因为新生代的对象通常是朝生夕灭的。

   如下图:

   5. STW

   G1 也是基于「标记-清除」算法,因此在进行垃圾回收的时候,仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以指定期望停顿时间。 

   如下图:

   G1 中存在三种 GC 模式,分别是 Young GC、Mixed GC 和 Full GC。

   1)Young GC

   当 Eden 区的内存空间无法支持新对象的内存分配时,G1 会触发 Young GC

   当需要分配对象到 Humongous 区域或者堆内存的空间占比超过 -XX:G1HeapWastePercent 设置的 InitiatingHeapOccupancyPercent 值时,G1 会触发一次 concurrent marking(并发标记),它的作用就是计算老年代中有多少空间需要被回收,当发现垃圾的占比达到 -XX:G1HeapWastePercent 中所设置的 G1HeapWastePercent 比例时,在下次 Young GC 后会触发一次 Mixed GC。

   2)Mixed GC

   Mixed GC 是指回收新生代的 Region 以及一部分老年代中的 Region。Mixed GC 和 Young GC 一样,采用的也是复制算法

   3)Full GC

   在 Mixed GC 过程中,如果发现老年代空间还是不足,此时如果 G1HeapWastePercent 设定过低,可能引发 Full GC。-XX:G1HeapWastePercent 默认是 5,意味着只有 5% 的堆是“浪费”的。如果浪费的堆的百分比大于 G1HeapWastePercent,则运行 Full GC。

  在以 Region 为最小管理单元以及所采用的 GC 模式的基础上,G1 建立了停顿预测模型,即 Pause Prediction Model 。这也是 G1 非常被人所称道的特性。

  我们可以借助 -XX:MaxGCPauseMillis 来设置期望的停顿时间(默认 200ms),G1 会根据这个值来计算出一个合理的 Young GC 的回收时间,然后根据这个时间来制定 Young GC 的回收计划。

  五、ZGC(Z Garbage Collector)

  ZGC 是 JDK11 推出的一款低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收,SPEC jbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。

  1. ZGC的设计目标

  ZGC 的设计目标是:在不超过 10ms 的停顿时间下,支持 TB 级的内存容量和几乎所有的 GC 功能。

  总之就是,ZGC 的目标是:

  • 停顿时间不超过 10ms;
  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
  • 支持 8MB~4TB 级别的堆,未来支持 16TB。

  前面讲 G1 垃圾收集器的时候提到过,Young GC 和 Mixed GC 均采用的是复制算法,复制算法主要包括以下 3 个阶段:

  1)标记阶段

  从 GC Roots 开始,分析对象可达性,标记出活跃对象。

  2)对象转移阶段

  把活跃对象复制到新的内存地址上。

  3)重定位阶段

  因为转移导致对象地址发生了变化,在重定位阶段,所有指向对象旧地址的引用都要调整到对象新的地址上。

  标记阶段因为只标记 GC Roots,耗时较短。但转移阶段和重定位阶段需要处理所有存活的对象,耗时较长,并且转移阶段是 STW 的,因此,G1 的性能瓶颈就主要卡在转移阶段

  与 G1 和 CMS 类似,ZGC 也采用了复制算法,只不过做了重大优化,ZGC 在标记、转移和重定位阶段几乎都是并发的,这是 ZGC 实现停顿时间小于 10ms 的关键所在。

  ZGC的几乎所有操作都是并发进行的,包括对象标记、重新标记、转移和清理,从而最大限度地减少停顿时间。此外,ZGC还支持并发压缩堆内存,减少内存碎片并提高内存利用效率。

  2.  ZGC 是怎么做到的呢?

  1)指针染色(Colored Pointer)

  一种用于标记对象状态的技术。ZGC使用64位指针中的高位进行标记,而不是使用全局对象表(如G1)来追踪对象状态,这样可以加速垃圾回收过程。

  2)读屏障(Load Barrier):

  一种在程序运行时插入到对象访问操作中的特殊检查,用于确保对象访问的正确性。 

  六、 Shenandoah 垃圾收集器

  1.  启用参数

  -XX:+UseShenandoahGC

  2. 新生代和老年代

  Shenandoah 同样不使用传统的分代模式,采用 Region 划分堆内存,针对整个堆内存进行并发标记、回收,旨在实现极低的停顿时间。

  3. 适用场景

  与 ZGC 类似,适合对低延迟有极高要求的场景。

  七、Epsilon 垃圾收集器

  1.  启用参数

  -XX:+UseEpsilonGC

  2.  新生代和老年代:Epsilon 是一个“无操作”的垃圾收集器,不进行垃圾回收,因此不会区分新生代和老年代。这主要用于性能基准测试。

  3. 适用场景

  测试或调试,不适用于生产环境。

 

 

  参考链接:

  https://yuanjava.com/cms/

  https://javabetter.cn/jvm/gc-collector.html#g1

posted @ 2024-10-30 12:02  欢乐豆123  阅读(29)  评论(0编辑  收藏  举报