ZGC随手记

第一部分:ZGC 的核心黑科技(底层原理)

要看懂流程,必须先看懂这三个概念的内部实现。

1. 染色指针 (Colored Pointers) ------ 指针不仅仅是地址

在传统的 Java 堆中,一个引用(Reference)就是一个纯粹的内存地址。

但在 ZGC 中,引用被改造了。ZGC 利用了 64 位指针中未使用的"高位"来存储垃圾回收的状态。

ZGC 指针结构(64位):
复制代码
  +-------------------+-+----+-----------------------------------------+
  |    Unused (18)    | |    |                                         |
  |                   |R|Mark|           Object Address (42)           |
  +-------------------+-+----+-----------------------------------------+
                       ^  ^
                       |  |
            Remapped --+  +-- Marked1 / Marked0
  • 0-41位 (42 bits): 对象的实际物理地址(支持 4TB,后来扩展到 16TB)。

  • 42-45位 (4 bits):颜色标志位(重点!)

    • Marked0 / Marked1: 标记位。用于标记对象是否是"活"的。为什么有两个?为了区分是"上一轮 GC 的标记"还是"这一轮 GC 的标记"。

    • Remapped: 重映射位。表示这个对象是否已经(或不需要)搬家,指针指向的是正确的现址。

    • Finalizable: 表示对象是否需要执行 finalize 方法(现在很少用了)。

核心逻辑:

ZGC 不去对象头里看状态,而是看你在持有这个对象的"引用"时,手里拿的这个指针是什么颜色

2. 多重映射 (Multi-Mapping) ------ 骗过 CPU

你可能会问:"指针里改了几个 bit,那这个地址不就变了吗?CPU 还能找到内存里的对象吗?"

ZGC 利用了操作系统的虚拟内存映射技术。它在虚拟内存层面,把同一个物理内存地址,映射到了三个不同的虚拟地址上(分别对应 Marked0、Marked1、Remapped)。

  • 也就是说,无论指针是 Marked0 + 地址A,还是 Remapped + 地址A,CPU 最终访问的都是同一个物理内存 A

  • 作用: 这样 Java 程序就能正常访问对象,而 GC 线程可以通过修改指针的高位来标记状态,互不干扰。

3. 读屏障 (Load Barrier) ------ 守门员

这是 ZGC 能并发工作的绝对核心。写屏障(Write Barrier)是在"赋值"时触发,而读屏障是在"读取"时触发。

每当你的代码执行类似 Object p = obj.field;(读取堆里的引用)时,JVM 注入的读屏障逻辑就会执行:

  1. 检查颜色(Test):p 的指针颜色和当前 GC 阶段的"正确颜色(Good Color)"对比。

  2. 快速路径(Fast Path): 如果颜色对上了,直接返回,几乎没有性能损耗。

  3. 慢速路径(Slow Path): 如果颜色不对(Bad Color),说明这个对象可能被移动了,或者还没被标记。

    • 自愈(Self-Healing): 读屏障会根据情况,去查"转发表",找到对象的新地址,然后立刻更新 你手里的引用 p

    • 返回: 更新完后,返回正确的对象给业务线程。


第二部分:ZGC 的超详细全流程解析

假设我们现在开始一次完整的 GC 循环。

阶段 0:准备工作

  • 全堆的指针颜色通常处于 Remapped 状态。

  • GC 选定本轮使用的标记位,假设是 Marked0

阶段 1:初始标记 (Pause Mark Start) ------ 【STW,极短】

  • 目标: 标记 GC Roots(线程栈变量、静态变量等)直接引用的对象。

  • 操作:

    1. 暂停所有线程。

    2. 扫描 Roots。

    3. 关键动作: 把 Roots 指向的对象的指针,从 Remapped 改为 Marked0

    4. 切换全局视图: 告诉所有读屏障,"从现在开始,Marked0 才是好颜色"。

  • 耗时: < 1ms。

阶段 2:并发标记 (Concurrent Mark) ------ 【并发,耗时最长】

  • 目标: 遍历对象图,找出所有存活对象。

  • 操作:

    • GC 线程从 Roots 开始,顺藤摸瓜。

    • 凡是摸到的对象,就把指向它的指针改为 Marked0

    • 读屏障的作用: 此时应用线程也在跑。如果应用线程加载了一个还没被标记的对象(颜色是旧的),读屏障会捕获它,帮它染成 Marked0,并加入标记队列。这保证了用户线程拿到的永远是"活"的对象。

阶段 3:再标记 (Pause Mark End) ------ 【STW,极短】

  • 目标: 处理并发阶段遗留的少量边缘情况(比如线程本地缓冲区里的引用)。

  • 操作: 彻底完成标记。此时,全堆所有存活对象的指针理论上都应该是 Marked0

阶段 4:并发准备重分配 (Concurrent Prepare for Relocate) ------ 【并发】

  • 目标: 选出哪些页面(ZPage)需要回收。

  • 操作:

    • 分析各个页面的垃圾比例。

    • 选出一组垃圾最多的页面,放入重分配集 (Relocation Set)

    • 注意:此时对象还没动。

阶段 5:初始重分配 (Pause Relocate Start) ------ 【STW,极短】

  • 目标: 仅仅处理 Roots 指向且位于"重分配集"里的对象。

  • 操作:

    1. 切换全局视图: 告诉所有读屏障,"从现在开始,Remapped 才是好颜色"。

    2. 扫描 Roots。如果某个 Root 指向的对象在"重分配集"里,GC 会把这个对象复制到新的空闲页面,并更新 Root 指针为新地址(带 Remapped 颜色)。

    3. 如果 Root 指向的对象不在 重分配集里,仅仅把指针颜色改为 Remapped

阶段 6:并发重分配 (Concurrent Relocate) ------ 【并发,核心难点】

  • 目标: 把"重分配集"里剩余的存活对象搬走,并利用读屏障实现"自愈"。

  • 操作:

    • GC 线程遍历"重分配集"里的所有对象。

    • 搬家: 发现存活对象,就把它复制到新页面。

    • 记录: 在旧页面的内存里建立一个转发表 (Forwarding Table) ,记录 旧地址 -> 新地址

    • 销毁: 搬完后,旧对象其实还在,只是成了垃圾。转发表保留,直到所有指向旧地址的指针都修正。

【高能场景演示:并发重分配时的"自愈"】

  1. 对象 A 在重分配集里,GC 线程把它搬到了新地址 A',并写了转发表 A -> A'

  2. 但是,堆里还有一个对象 B,它的字段 B.field 仍然指向旧地址 A(因为还没来得及修)。

  3. 用户线程 执行代码 Object x = B.field;

  4. 触发读屏障:

    • 检测到 x 的指针颜色是 Marked0(坏颜色,因为阶段 5 已经把好颜色改成 Remapped 了)。

    • 进入慢速路径。

  5. 查表: 读屏障去查转发表,发现 A 已经搬到了 A'

  6. 修正(自愈):

    • 读屏障把 B.field 的引用更新为 A'(带 Remapped 颜色)。

    • 返回 A' 给用户线程。

  7. 后续: 下次再访问 B.field,颜色是对的,直接走快速路径。


第三部分:总结 ZGC 与 G1 的本质区别

为了方便记忆,请看这个对比:

特性 G1 (Garbage First) ZGC (Z Garbage Collector)
内存管理 Region:物理不连续,大小固定。 ZPage:物理不连续,大小动态(小/中/大)。
存活判断 BitMap:用额外的位图记录存活状态。 Colored Pointers:状态直接写在指针里。
并发保障 SATB + 写屏障:在写入时记录旧引用,防止漏标。 读屏障:在读取时检查指针颜色,实现自愈。
对象移动 STW 期间移动:必须暂停所有线程才能搬对象。 并发移动:线程一边跑,对象一边搬(靠转发表和读屏障)。
性能损耗 写屏障开销较小,但 STW 时间随堆增大而增加。 读屏障开销较大(吞吐量低 10%),但 STW 几乎恒定且极低。

一句话总结 ZGC 的流程:

利用染色指针 标记状态,利用多重映射 欺骗 CPU,利用读屏障 在对象移动后实现引用的懒加载式修复(自愈),从而将停顿时间从"搬家时间"缩减为"改个路牌的时间"。

相关推荐
好家伙VCC2 小时前
# BERT在中文文本分类中的实战优化:从模型微调到部署全流程在自然语言处理(NL
java·python·自然语言处理·分类·bert
只会写bug的小李子2 小时前
AI Agent动态规划失效处理:多步执行卡壳时,局部修正远比从头重来更高效
java·开发语言
NGC_66112 小时前
idea中使用git
java·git·intellij-idea
Renhao-Wan2 小时前
Java 算法实践(三):双指针与滑动窗口
java·数据结构·算法
Pluchon2 小时前
硅基计划4.0 算法 图的存储&图的深度广度搜索&最小生成树&单源多源最短路径
java·算法·贪心算法·深度优先·动态规划·广度优先·图搜索算法
我命由我123452 小时前
Kotlin 面向对象 - 匿名内部类、匿名内部类简化
android·java·开发语言·java-ee·kotlin·android studio·android jetpack
学到头秃的suhian2 小时前
Redis分布式锁
java·数据库·redis·分布式·缓存
星火开发设计2 小时前
模板特化:为特定类型定制模板实现
java·开发语言·前端·c++·知识
wzqllwy2 小时前
Java实战-性能
java