12垃圾回收器

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

在以下要分析收集器的时候,会涉及到并行,串行,并发的概念,做如下解释:

并行(Parallel):指多条垃圾回收线程并行工作,但此时用户线程仍然处于等待状态

串行(serial):相较于并行的概念而言,它是单线程执行,如果内存不够,则程序暂停,启动jvm垃圾回收器进行垃圾回收,回收完,再启动程序的线程

并发(concurrent):指用户线程与垃圾回收线程同时执行(但不一定并行,可能是交替执行),垃圾回收线程在执行时是不会停顿用户程序的运行 eg:CMS G1

4.4.1垃圾收集器发展史

  1. 1999年,随JDK 1.3.1 一起来的是串行方式的 Serial GC ,它是第一款GC。ParNew GCSerial GC 的多线程版本。
  2. 2002年2月26日,Parallel GCConcurrent Mark Sweep GC( 即 CMS ) 跟随 JDK1.4.2 一起发布。
  3. Parallel GC 在 JDK6 之后成为 Hotspot 默认GC。
  4. 2012年,在 JDK1.7u4 中,G1 可用。
  5. 2017年,JDK9 中 G1 成为默认垃圾收集器,以替代 CMS从此后,JDK每半年更新一次
  6. 2018年3月,JDK10 中 G1 的并行完整垃圾回收,实现并行性能改善最坏情况的延迟。
  7. 2018年9月,JDK11 发布。引入 EpsilonGC ,又称为“No-Op(无操作)” 回收器;同时引入 ZGC: 可伸缩的低延迟回收器(Experimental)。
  8. 2019年3月,JDK12 发布。增加 G1,自动返回未使用堆内存给操作系统; 同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。
  9. 2019年9月,JDK13 发布。增强 ZGC,自动返回未使用堆内存给操作系统。
  10. 2020年3月,JDK14 发布。删除 CMS。扩展 ZGC 在 mac 和 windows 的应用
  11. 2021年9月,JDK 17 发布(LTS版本):进一步增强各GC性能改进了 G1 GCParallel GC 的可用性
  12. 2022年3月,JDK 18 发布:主要进行GC性能优化和错误修复 没有引入新的GC特性
  13. 2022年9月,JDK 19 发布:引入虚拟线程(预览版)改进了 Generational Z GC(分代式ZGC,实验性功能)
  14. 2023年3月,JDK 20 发布:继续改进 Generational Z GC(仍为实验性)增强各GC与虚拟线程的协作
  15. 2023年9月,JDK 21 发布(LTS版本):Generational Z GC(分代式ZGC)正式成为生产特性引入分代模式的 Shenandoah GC进一步优化 G1 GC 的性能和资源利用率

4.4.2 查看当前所收集器

-XX:+PrintCommandLineFlags

**4.4.3. Serial/**SerialOld-串行回收(客户端单cpu)

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)适用于单核CPU的服务器(客户端等)

它还有对应老年代的版本:Serial Old

参数控制: -XX:+UseSerialGC 串行收集器

image-20251101140049681

这个收集器是一个单线程的收集器,但它的”单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束(STW)

4.4.4.ParNew-并行回收

ParNew收集器其实就是Serial收集器的多线程版本。Par是parallel的缩写,New:只能处理的是新生代 所以它的特点是: 新生代并行(有多条垃圾回收线程),老年代串行;新生代复制算法、老年代标记-压缩

ParNew收集器除了采用并行回收的方式执行内存回收外,与serial收集器几乎没有任何区别

参数控制:

-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量

image-20251102000823570

对于新生代,回收次数频繁,使用并行方式高效

对于老年代,回收次数少,使用 串行方式节省资源(cpu并行需要切换线程,串行可以省去切换线程的资源)

在收集器组合关系图中

ParNew+Serial Old 遗憾的是在JDK9中,组合被遗弃

parNew+CMS 遗憾的是在JDK14中CMS被移除

遗憾的是ParNew 也会逐渐被放弃~~~

image-20251106080951548

4.4.5. Parallel-吞吐量优先

Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;

新生代复制算法、老年代标记-压缩 但同样也是基于并行回收和STW机制

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

参数控制: -XX:+UseParallelGC 使用Parallel收集器+ 老年代串行

4.4.6. Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-压缩”算法。这个收集器是在JDK 1.6中才开始提供

参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

4.4.7. CMS收集器-低延迟

在JDk1.5时期,HotSpot推出了一款在强交互应用中有划时代意义的垃圾收集器CMS(Concurrent-Mark-Sweep),这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

不幸的是,CMS作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器parallel scavenge 配合工作,所以在jdk1.5中使用的CMS来收集老年代的时候,新生代只能选择ParNew或者serial收集器中的一个

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

image-20251102000520953

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

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。CMS属于老年代收集器(新生代使用ParNew)

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

​ 会产生内存碎片碎片会导致并发清除之后,用户线程可用的空间不足,在无法分配大对象的情况下,不得不提前触发 Full GC,也是CMS收集器所存在的一种定时炸弹,在某种极端情况下,会启动serial Old 收集器来重新进行老年代的垃圾回收,这样STW就会变长

小结:

​ 如果你想要最小化地使用内存和并行开销,请选serial GC

​ 如果你想要最大化应用程序的吞吐量,请选Parallel GC

​ 如果你想要最小化GC的中断或停顿时间,请选CMS GC + ParNew

但遗憾的是,在jdk9中被标记为过时,在jdk14中,已经将CMS废弃

4.4.8. G1收集器

4.4.8.1.简介

G1全称 Garbage First(垃圾优先)

G1是在java7 之后引入的一个新的收器

核心设计思想

  • 化整为零,分区回收:G1 不再坚持物理上连续的新生代、老年代划分。 它将整个Java堆划分为多个大小相等的独立区域(Region,默认约2048个,每个Region大小1MB~32MB)不同区域可同时回收,并发性更高。
  • 垃圾优先:其名称即其策略。G1 会持续跟踪每个 Region 中垃圾的“价值”(即回收所能释放的空间大小以及回收所需的时间),并优先回收那些垃圾最多、回收收益最大的 Region。这就像清洁工总是先清理最脏、最容易清理的房间。
  • 可预测的停顿时间模型:这是 G1 的核心目标。用户可以通过参数 -XX:MaxGCPauseMillis(默认200ms)设置一个期望的最大停顿时间目标。G1 会尽力在这个目标范围内完成垃圾回收,避免出现一次非常长的停顿(这也是 CMS 的痛点)

4.4.8.2. Region概念

G1 不再坚持物理上连续的新生代、老年代划分。 它将整个Java堆划分为多个大小相等的独立区域(Region,默认约2048个,每个Region大小1MB~32MB)不同区域可同时回收,并发性更高

G1 的对象管理

区域名称 存放对象 颜色体现
Eden Regions 用于分配新对象 红色部分
Survivor Regions 存放 Minor GC 后存活的对象 红色标记为s
Old Regions 存放经历多次 GC 后依然存活的对象(晋升对象) 淡蓝色
Humongous Regions 如果一个对象的大小超过了 Region 容量的一半它就会被认为是一个巨型对象,并被直接分配在一组连续的 Humongous Regions 带有H字样

4.4.8.3 卡表和记忆集(解决跨代引用)

G1 是分区(Region)管理的收集器,堆被划分成很多 Region。

在回收某些 Region(比如年轻代 Region)的时候,需要知道哪些对象有跨 Region 的引用

  • 否则就得全堆扫描,代价太大。

所以就引入了 Remembered Set 来记录“外部 Region 指向本 Region 的引用”。而记录这些引用的基础设施就是 Card Table(实现记忆集)

image-20251102111930761

Remember Set 不是直接记录对象地址,而是记录对象所在的 Card 编号。

所谓 Card 就是表示一小块(512 字节)的内存空间,这里面很可能存在不止一个对象

当我们需要确定当前 Region 有哪些对象存在外部引用时(这些对象是可达的,不能被回收),只要扫描一下这块 Card 中的所有对象即可

实现上,Remember Set 的实现就是一个 Card 的 Hash Set,并且为每个 GC 线程都有一个本地的 Hash Set,最后的 Remember Set 实际上是这些 Hash Set 的并集

Card Table 的作用

卡表如何工作?

  1. 对象写入时

    1
    obj.field = newObj
    • 如果 obj 在老年代,newObj 在年轻代 → 触发卡表标记
    • GC更新卡表:card_table[card_index] = 1(标记该卡片为”脏”)。

​ 当某个对象字段被写入时(JVM 的 写屏障)G1 的 Write Barrier 实际上只是一个“通知”,会把对应 Card 标记为 dirty(脏卡片)。这样就能快速知道:哪些内存区域(Card)(一大块的内存空间)里可能有新的跨 Region 引用

Remembered Set 的作用

​ G1 给每个 Region 建立一个 Remembered Set

​ 这个集合记录了 哪些 Card 里包含指向该 Region 的引用

​ 在回收某个 Region 时,就只需要遍历它的 Remembered Set,找到这些跨区引用,而不用全堆扫描

关于写屏障的说明 写屏障是JVM在对象引用赋值时插入的一小段“钩子”代码,它像哨兵一样监视着所有对象引用关系的变化,并为并发垃圾收集器(如G1)提供关键信息,以确保标记的正确性检查是否跨Region(oldObj在老年代,youngObj在年轻代)

话术

Remembered Set 来记录“外部 Region 指向本 Region 的引用”。这些引用的基础设施就是 Card Table

Remember Set 不是直接记录对象地址,而是记录对象所在的 Card 编号。

Card Table 的作用(将堆划分为512字节的卡片,用字节标记是否被修改)

​ 当某个对象字段被写入时(JVM 的 写屏障)G1 的 Write Barrier 实际上只是一个“通知”,会把对应 Card 标记为 dirty(脏卡片)。这样就能快速知道:哪些内存区域(Card)(一大块的内存空间)里可能有新的跨 Region 引用

GC准备阶段

  • G1扫描卡表,找出所有被标记为脏的卡片
  • 将这些卡片对应的引用添加到目标区域的记忆集中。

Remembered Set 的作用

​ G1 给每个 Region 建立一个 Remembered Set

​ 这个集合记录了 哪些 Card 里包含指向该 Region 的引用

​ 在回收某个 Region 时,就只需要遍历它的 Remembered Set,找到这些跨区引用,而不用全堆扫描

4.4.8.4 G1回收周期

G1中标记-复制算法 即新生区用复制算法 养老区用标记压缩算法

image-20251102115634782

G1的混合回收过程可以分为标记阶段、清理阶段和复制阶段。

标记阶段停顿分析

  • 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
  • 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
  • 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。

清理阶段停顿分析

  • 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。

复制阶段停顿分析

  • 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。

四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。

4.4.8.5 并发标记之三色标记法

在 G1 里,三色标记法是并发标记阶段用来 判断对象存活性 的核心算法

白色 (White) 未被垃圾收集器访问到的对象。 可达性未知的对象。 在标记开始时,所有对象都是白色。随着标记进行,仍然为白色的对象就是不可达的,即垃圾。 被回收(Reclaimed)
灰色 (Grey) 对象本身已被垃圾收集器访问到(标记为存活),但它的所有字段引用到的对象还没有被检查。 待扫描的对象。 灰色对象是标记过程的“前沿”,它意味着标记还未完成,后面还有工作要做。灰色对象是存活的。 存活(Survived),最终会变为黑色。
黑色 (Black) 对象本身已被垃圾收集器访问到,并且它的所有字段引用到的对象也都被检查过了。 已扫描完成的对象。 黑色对象是确定存活的,并且不会再被重新检查(除非发生特殊情况)。 存活(Survived)

示意图如下:

image-20251102135347365

三色标记的过程如下:

  1. 初始阶段,所有对象都是白色。
  2. 将 GC Roots 直接引用的对象标记为灰色。
  3. 处理灰色对象,把当前灰色对象引用的所有对象都变成灰色,之后将当前灰色对象变成黑色。
  4. 重复步骤 3,直到不存在灰色对象为止。

三色标记结束后,白色对象就是没有被引用的对象(比如上图中的 H 和 G),可以被回收了。

上面的过程是串行的、STW的。但G1的标记是并发的(标记线程与应用线程同时运行),这会导致一个问题:在标记过程中,应用线程可能改变了对象间的引用关系

主要会产生两种问题:

  1. 把已标记的对象误标为垃圾(漏标):一个白色对象,在并发过程中被一个黑色对象重新引用了。因为黑色对象不会再被扫描,这个白色对象就会被错误地当作垃圾清理掉。这是严重错误,必须解决。
  2. 把已经不存活的对象误标为存活(多标):一个对象被解除了引用,但因为它已经被扫描过(变黑了),所以不会被回收。这会产生一些“浮动垃圾”,可以接受,下次GC清理即可。

G1 使用 SATB(Snapshot-At-The-Beginning) 写屏障技术来解决漏标问题

SATB 原理:

  • 在标记开始时,G1 会为堆内存创建一个逻辑上的“快照”。
  • 每当应用线程要断开一个对象的引用时(例如:obj.field = nullobj.field = new_value),写屏障会被触发。
  • 这个写屏障会将被覆盖的旧引用值(即将被断开连接的那个对象)记录下来,并将其压入一个特殊的队列
  • 在后续的 Remark 阶段,G1 会再次STW,然后重新扫描这个队列里的所有对象。如果这个对象是白色的,它就会被强制标记为灰色(或黑色),从而在本轮收集中得以存活。

SATB 保证:只要在标记开始时是存活的对象,就会被视为存活,即使并发过程中引用被断开

4.4.8.6 G1 的 GC 模式

GC 模式指的是 在什么情况下触发 GC,以及 GC 的目标是什么

GC 模式 触发条件 收集范围 目标 特点
Young GC Eden 区满 年轻代 快速回收年轻代 频繁,STW 时间
Mixed GC 并发标记完成 年轻代 + 部分老年代 高效释放老年代内存 阶段性,STW 时间可控
Full GC 回收失败 整个堆 补救措施,避免 OOM 应避免,STW 时间非常长

调优建议: G1 的调优核心是 为 Mixed GC 创造有利条件,即通过合理设置 -XX:InitiatingHeapOccupancyPercent、调整堆大小、优化对象创建速度等手段,确保并发标记周期能及时启动并在老年代填满之前完成,让 Mixed GC 有足够的时间来回收内存,从而彻底避免 Full GC 的发生

4.4.8.6 G1的使用场景

判断是否需要使用G1,它的目标是为拥有以下特征的应用在运行时达到最短的延迟更高吞吐量平衡点 :

  • **(1). 超过8GB的大堆内存,**并且堆中有一半以上的存活数据,对大堆内存空间的性能出色;

    • 大堆和多核心cpu;
    • 因为是分区region选择扫描,可以避免扫描大堆空间,减少停顿时间;
  • (2). 对象的分配和晋升速率随着时间变化非常快;

    • 还是避免扫描大堆空间;region扫描,而不是整个堆空间,性能高,时间可控;
  • (3). 堆内存在大量的内存碎片的情况

  • **(4). 需要可预测的gc停顿时间,**希望gc时长不会超过几百毫秒,避免长时间的gc停顿;

  • 要求可控的停顿时间和吞吐量,一般设置gc时间在300-500ms范围;

4.4.9. ZGC

4.4.9.1 简介

https://wiki.openjdk.org/display/zgc/Main

The Z Garbage Collector是JDK 11中推出的一款低延迟垃圾回收器,并在 JDK 15 中被宣布为可用于生产环境

ZGC 本质上是一个并发垃圾收集器,这意味着所有繁重的工作都是在 Java 线程继续执行时完成的

4.4.9.2 Region

跟 G1 类似,ZGC 的堆内存也是基于 Region 来分布,不过 ZGC 是不区分新生代老年代的,ZGC 的 Region 支持动态地创建和销毁,并且 Region 的大小不是固定的,包括三种类型的 Region :

  • Small Region:2MB,主要用于放置小于 256 KB 的小对象。
  • Medium Region:32MB,主要用于放置大于等于 256 KB 小于 4 MB 的对象。
  • Large Region:N * 2MB。这个类型的 Region 是可以动态变化的,不过必须是 2MB 的整数倍,最小支持 4 MB。每个 Large Region 只放置一个大对象,并且是不会被重分配的。

4.4.9.3 内存多重映射

大白话解释:

就像给同一套房子的不同房间挂了不同的门牌号,但实际都是同一套房子。

详细说明:

  • ZGC 把同一块物理内存同时映射到虚拟内存的三个不同地址区域
  • 这三个地址区域分别代表对象的:当前地址、转移中地址、已转移地址
  • 好处:对象在内存中搬家时,不需要立即更新所有指向它的指针,因为通过任何一个”门牌号”都能找到它

图例

image-20251105211838043

把同一块儿物理内存映射为 Marked0、Marked1 和 Remapped 三个虚拟内存

当应用程序创建对象时,会在堆上申请一个虚拟地址,这时 ZGC 会为这个对象在 Marked0、Marked1 和 Remapped 这三个视图空间分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址

Marked0、Marked1 和 Remapped 这三个虚拟内存作为 ZGC 的三个视图空间,在同一个时间点内只能有一个有效。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收

举例:
你的对象住在”北京路100号”,ZGC要给它搬家到”上海路200号”。普通GC需要通知所有认识这个对象的人:”我搬家了,新地址是上海路200号”。而ZGC同时保留两个地址都能找到这个对象,等有空的时候再慢慢通知大家。

Mk0 初始映射空间(原始对象所在的虚拟页) GC 周期开始时,所有对象都在 Mk0 GC 复制开始前,对象在 Mk0 区访问
Mk1 新映射空间(复制目标区) 在 GC 复制(Relocation)阶段使用 GC 把对象从 Mk0 复制到 Mk1,对象新地址在 Mk1
Remapped 重映射空间,用于访问已被移动对象 GC 复制完成后 当逻辑地址仍指向旧位置(Mk0),但对象已被复制时 对象依然存活,访问会跳转到 Mk1 的对应地址

Mk0:对象当前物理位置(旧的)。

Mk1:对象新复制的物理位置(新的)。

Remapped:用于访问对象的最终逻辑映射(自动跳转到正确位置)。

4.4.9.4 染色指针技术

大白话解释:

就像在快递单号上直接用颜色标记包裹状态,而不是另外建个表格记录。

详细说明:

  • 传统GC: GC 信息保存在对象头的 Mark Word 中。比如 64 位的 JVM,对象头的 Mark Word 中保存的信息如图

    前 62位保存了 GC 信息,最后两位保存了锁标志。

image-20251105214621445

  • ZGC:直接在指针的64位地址中拿出4位来标记状态(就像在地址上”涂颜色”)将 GC 信息保存在了染色指针上。染色指针是一种将少量信息直接存储在指针上的技术。在 64 位 JVM 中,对象指针是 64 位,如下图

在这个 64 位的指针上,高 16 位都是 0,暂时不用来寻址。剩下的 48 位支持的内存可以达到 256 TB(2 ^48),这可以满足多数大型服务器的需要了。不过 ZGC 并没有把 48 位都用来保存对象信息,而是用高 4 位保存了四个标志位,这样 ZGC 可以管理的最大内存可以达到 16 TB(2 ^ 44)

通过这四个标志位,JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。

无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率

image-20251105215457649

  • 这4位可以表示:是否标记、是否已重定位等状态

举例:
普通快递:包裹编号”12345”,状态”已发货”需要查系统才知道。
ZGC快递:包裹编号直接写成”🚚12345”,一看就知道在运输中。不用查系统,看编号颜色就知道状态。

染色指针技术和内存多重映射关系

在 64 位地址空间下,ZGC 使用指针的高位来编码颜色信息(比如 Remapped 标志位),再通过地址映射表将逻辑地址映射到正确的物理内存页

4.4.9.5 读屏障

大白话解释:

就像图书馆管理员,在你借书时检查这本书是不是正在被重新整理上架。

读屏障会对应用程序的性能有一定影响,据测试,对性能的最高影响达到 4%,但提高了 GC 并发能力,降低了 STW。

详细说明:

  • 读屏障是一小段检查代码,在程序每次读取对象引用时自动执行
  • 主要检查:这个指针的”颜色”(状态标记)是否正常
  • 如果发现对象正在被移动,就等待移动完成或者帮忙完成移动

举例:
你去图书馆借《三国演义》,管理员(读屏障)发现这本书正在从”历史区”搬到”文学区”。他会:

  1. 要么告诉你:”稍等,书正在搬,搬完再借”
  2. 要么直接帮你把书从搬运车上拿下来给你

4.4.9.6 ZGC回收周期

image-20251105231648198

  • 初始化:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段。
  • 并发标记阶段:第一次进入标记阶段时视图为Mark0,如果对象被GC标记线程或者应用线程访问过,那么就将对象的地址视图从Remapped调整为Mark0。所以,在标记阶段结束之后,对象的地址要么是M0视图,要么是Remapped。如果对象的地址是Mark0视图,那么说明对象是活跃的;如果对象的地址是Remapped视图,说明对象是不活跃的。
  • 并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过,那么就将对象的地址视图从M0调整为Remapped。

ZGC只有三个STW阶段:初始标记再标记初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

4.4.9.7 ZGC存在的问题

ZGC最大的问题是浮动垃圾。

浮动垃圾

ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。

JDK21以及之前,ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。

解决方案

目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。

4.4.10. 垃圾回收器比较

image-20251106080951548

如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。

垃圾回收器选择策略 :

客户端程序 : Serial + Serial Old;

吞吐率优先的服务端程序(比如:计算密集型) : Parallel Scavenge + Parallel Old;

响应时间优先的服务端程序 :ParNew + CMS G1。

G1收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)。


12垃圾回收器
http://example.com/2025/11/14/12垃圾回收器/
作者
無鎏雲
发布于
2025年11月14日
许可协议