基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

ZGC 垃圾收集器的工作原理

知识点图片

ZGC (Z Garbage Collector) 是由 Oracle 开发的一款具有划时代意义的超低延迟垃圾收集器。它最早在 JDK 11 中作为实验特性引入,在 JDK 15 中转为正式特性。

ZGC 的核心设计目标是:无论堆内存多大(从 8MB 到 16TB),垃圾回收的停顿时间(STW)都不会超过 1 毫秒(早期目标为 10ms,JDK 16 优化到了 1ms 以下)

为了实现这个近乎苛刻的目标,ZGC 抛弃了传统 GC 的很多包袱,引入了两个极具创新性的核心技术:染色指针(Colored Pointers)读屏障(Load Barrier)

下面为你深度解析 ZGC 的工作原理。


一、 ZGC 的核心基石(它是如何做到的?)

1. 染色指针(Colored Pointers)

传统的 GC(如 G1、CMS)通常把对象的标记信息(比如对象是不是垃圾、有没有被移动过)记录在对象头里。
ZGC 别出心裁,把这些标记信息直接记录在指针(引用地址)上。

在 64 位系统中,指针有 64 位。ZGC 利用了其中的高几位来做标记:

  • 18 位:预留未使用。
  • 4 位(颜色标志位):包含 Marked0Marked1RemappedFinalizable。这就是“染色”的由来。
  • 42 位(对象地址):用于寻址,支持最大 4TB 的堆内存(JDK 13 之后扩展到 44 位地址,支持 16TB)。

为什么要在指针上染色?
因为这样 JVM 在判断一个对象的状态时,不需要去读取对象所在的内存,只需要看指针本身即可。这极大地减少了内存访问延迟(避免了 CPU 缓存未命中 Cache Miss)。

(注:由于标记了 M0 和 M1 两个视图,ZGC 会在两次连续的 GC 周期中交替使用它们,以此来区分“上一次 GC 存活的对象”和“本次 GC 存活的对象”。)

2. 读屏障(Load Barrier)与 指针自愈(Self-Healing)

传统的 GC(如 G1)大量使用写屏障(Write Barrier),而 ZGC 几乎只使用读屏障

读屏障是指:当 Java 应用程序线程从堆中读取一个对象引用时,JIT 编译器会插入一小段代码来检查这个引用的状态。

结合染色指针,它的工作流程是(指针自愈机制):

  1. 业务线程尝试读取一个指针。
  2. 读屏障拦截,检查指针的“颜色”。
  3. 如果发现指针的颜色是 Remapped(或者当前正常的颜色),说明对象状态没问题,直接返回。
  4. 如果发现指针颜色不对(比如对象刚刚被 ZGC 移动到了新的位置,但旧指针还没来得及更新),读屏障会触发一个操作:去“转发表(Forwarding Table)”里查找该对象的新地址,更新当前读取的指针,并返回新地址。
  5. 这个过程被称为指针自愈。下次再访问这个指针时,就不会再触发修复操作了。

依靠指针自愈,ZGC 可以在业务线程运行的同时,把对象搬迁到新的内存位置,而不需要长时间的 STW 暂停。


二、 ZGC 的内存布局

ZGC 也是基于 Region(区域)的,但它称之为 Page(页面)。与 G1 固定大小的 Region 不同,ZGC 的 Page 大小是动态的,分为三类:

  • 小型 Page(Small):2MB,存放小于 256KB 的对象。
  • 中型 Page(Medium):32MB,存放 256KB ~ 4MB 的对象。
  • 大型 Page(Large):容量不固定(但必须是 2MB 的整数倍),存放大于 4MB 的对象。一个大型 Page 只存放一个大对象,且不会被重分配(Relocate)。

三、 ZGC 的垃圾回收过程(生命周期)

ZGC 的一个完整回收周期主要包含以下几个阶段(重点关注并发阶段):

1. 初始标记(Pause Mark Start) - 【STW】

  • 耗时:极短(通常 < 1ms)。
  • 做啥:仅仅扫描所有 GC Roots(如线程栈、局部变量等)直接引用的对象。因为 Roots 数量极少,所以停顿时间不仅短,而且与堆大小无关

2. 并发标记(Concurrent Mark) - 【并发】

  • 耗时:较长,但与业务线程并发。
  • 做啥:从 GC Roots 开始遍历整个对象图,可达的对象指针会被染上 Marked0Marked1 的颜色。如果并发标记期间业务线程修改了引用,会通过读屏障保证标记的正确性。

3. 最终标记(Pause Mark End) - 【STW】

  • 耗时:极短。
  • 做啥:处理并发标记期间遗留的一些极少量的状态(如处理线程的本地缓冲)。

4. 并发准备转移(Concurrent Prepare for Relocate) - 【并发】

  • 做啥:ZGC 扫描所有的 Page,评估哪些 Page 里的存活对象较少,适合被清理。把这些 Page 选出来组成转移集(Relocation Set)

5. 初始转移(Pause Relocate Start) - 【STW】

  • 耗时:极短。
  • 做啥:转移 GC Roots 直接引用的对象。一旦转移完成,业务线程就能继续运行了。

6. 并发转移(Concurrent Relocate) - 【并发 / ZGC 的灵魂】

  • 做啥:GC 线程把转移集(Relocation Set)里的存活对象,复制到新的空白 Page 中,并在转发表(Forwarding Table)中记录“旧地址 -> 新地址”的映射关系。
  • 关键点:如果在这个过程中,业务线程访问了还没被转移的旧对象,或者访问了已经转移但指针还没更新的旧对象,会怎么样?
    • 这就是读屏障指针自愈发挥作用的时候!读屏障会发现指针颜色不对,去转发表查到新地址,把指针修正为新地址,然后业务线程继续操作新对象。
  • 一旦转移集里的所有对象都被转移完毕,旧的 Page 就可以被立即释放用于分配新对象。由于指针修正由读屏障按需完成,ZGC 不需要为了修正全堆的指针而进行 STW 暂停。

四、 ZGC 的演进:分代 ZGC(Generational ZGC)

  • 单代 ZGC 的痛点(Java 11 - 20):早期的 ZGC 不区分年轻代和老年代。每次 GC 都要扫描整个堆。如果应用程序分配对象的速度(Allocation Rate)极快,ZGC 回收垃圾的速度赶不上分配的速度,就会导致分配停顿(Allocation Stall)
  • 分代 ZGC(Java 21 引入):Java 21 正式推出了分代 ZGC。它像传统 GC 一样区分了年轻代和老年代,频繁且快速地回收年轻代(绝大多数对象朝生夕死)。这极大地降低了 CPU 的消耗,提高了系统的吞吐量,同时依然保持了亚毫秒级的延迟。

五、 总结:ZGC 为什么这么强?

  1. 极低的 STW:STW 只发生在扫描和转移 GC Roots 时,耗时都在微秒级/亚毫秒级,且停顿时间与堆大小完全无关(哪怕 16TB 的堆,STW 也是零点几毫秒)。
  2. 并发复制:依靠染色指针 + 读屏障的指针自愈技术,ZGC 能够在并发情况下安全地移动对象并整理内存碎片。
  3. 适用场景
    • 对延迟极其敏感的应用(如量化交易系统、实时竞价系统、在线游戏、大容量缓存系统)。
    • 堆内存超大的应用(几十 GB 到几 TB)。
  4. 代价与妥协
    • 吞吐量下降:由于读屏障的持续存在和大量的并发 GC 线程抢占 CPU,ZGC 牺牲了部分吞吐量(通常比 G1 低一些,尤其是在 CPU 核心数较少的机器上)。
    • 内存消耗:转发表和多视图映射等机制会额外占用一部分系统物理内存。
00:00
00:00