Skip to content

GC 读/写屏障与 ZGC 并发转移 ​

标签
语言/Java
语言/Java/JVM
字数
8499 字
阅读时间
34 分钟

先明确:GC中的「读/写屏障」到底是什么? ​

首先要摒弃“屏障是物理硬件”的误区,GC的读/写屏障是JVM层面的「内存访问拦截器」,与 AOP 思想一致:

  • 写屏障:在应用线程执行「引用赋值」(如obj.foo = new Object();/a = b;)时,JVM自动插入的一段额外执行逻辑(代码),拦截这个写操作,在赋值前后做一些GC相关的辅助工作;
  • 读屏障:在应用线程执行「引用读取」(如Object a = obj.foo;/if (b != null))时,JVM自动插入的一段额外执行逻辑,拦截这个读操作,在读取后做一些GC相关的辅助工作。

核心共性:都是JVM对应用线程内存操作的无感知拦截(应用代码无需修改,JVM底层自动处理),目的是让GC线程和应用线程能「并发工作」,减少STW(Stop The World)——本质是用少量应用线程的性能开销,换取GC并发能力的提升。

通俗类比:

  • 写屏障 = 你往快递柜放快递(写操作)时,快递柜自动触发的「扫码登记」逻辑(额外工作),不影响你放快递,但会记录快递信息;
  • 读屏障 = 你从快递柜取快递(读操作)时,快递柜自动触发的「验货核对」逻辑(额外工作),不影响你取快递,但会确认快递是否被调仓/转移。

一、写屏障:CMS/G1的核心,「写操作时做GC辅助」 ​

写屏障是分代GC/并发GC的基础,CMS和G1的核心并发能力都依赖写屏障,且二者的写屏障定位不同(CMS解决并发标记问题,G1解决并发标记+跨代引用问题),但核心逻辑都是拦截「引用写操作」。

1. 写屏障的核心适用场景:GC的「并发标记阶段」 ​

GC的核心工作分为标记(找到堆中所有存活对象)、清理/转移(回收垃圾对象/移动存活对象)两大阶段,其中标记阶段最容易和应用线程并发,而写屏障的核心作用就是保证「并发标记的准确性」——解决并发标记时的**「漏标问题」**。

为什么会有漏标问题?(写屏障的设计初衷) ​

GC线程在并发标记存活对象时,应用线程还在运行,会不断修改对象引用(写操作),可能导致GC线程漏标存活对象,最终把存活对象误判为垃圾回收,引发OOM或程序崩溃。

举个漏标场景:

  1. GC线程标记到对象A,判定A是存活的,但还没标记A的引用属性A.foo = B(对象B);

  2. 应用线程执行写操作:A.foo = null;(断开A到B的引用),同时执行C.foo = B(让对象C引用B);

  3. GC线程后续标记到C时,若没发现C到B的新引用,会误判B是垃圾并回收。

写屏障的解决思路:拦截所有引用写操作,记录下「被修改的引用关系」,GC线程后续通过这些记录补充标记,避免漏标——相当于给应用线程的写操作装了个「监控摄像头」,所有引用变化都被记录,GC线程不会错过任何存活对象。

2. CMS的写屏障:仅解决「并发标记漏标」(增量更新) ​

CMS是第一款真正意义上的并发GC(针对老年代),其写屏障采用**「增量更新」**策略,核心逻辑:

当应用线程修改老年代对象的引用时,写屏障会将「被修改的老年代对象」标记为「脏对象」,加入「脏队列」;GC线程在并发标记结束后,会重新标记脏队列中的对象,补充标记被修改的引用关系,避免漏标。

CMS写屏障的触发时机:仅拦截老年代对象的引用写操作(新生代用ParNew收集,和CMS配合,无需写屏障),轻量且开销小。

3. G1的写屏障:更复杂,解决「并发标记+跨代引用」(SATB) ​

G1是区域化的GC(将堆划分为多个大小相等的Region),没有明确的新生代/老年代边界,因此写屏障的职责更重,采用**「SATB(Snapshot At The Beginning):初始快照」**策略,核心逻辑:

并发标记开始时,对堆中所有存活对象做一个「快照」;当应用线程修改任何对象的引用时,写屏障会将「被覆盖的旧引用」记录下来,加入「SATB队列」;GC线程后续会根据旧引用补充标记,保证标记结果和快照时刻一致,彻底避免漏标。

额外职责:处理「跨代引用」(Remembered Set,记忆集) ​

G1的每个Region都有一个Remembered Set(RS,记忆集),写屏障还有一个核心工作:

当应用线程执行跨Region的引用写操作(如新生代Region的对象引用老年代Region的对象)时,写屏障会将这个引用关系记录到目标Region的RS中;GC回收某个Region时,只需扫描自己的RS,无需扫描整个堆,大幅提升GC效率。

G1写屏障的特点:拦截所有对象的引用写操作,逻辑比CMS更复杂,性能开销比CMS写屏障大(但远小于读屏障)。

4. 关键:G1用写屏障,为什么转移对象时必须STW? ​

这里的核心点是:G1 的对象转移(整理)阶段(GC的「转移/清理阶段」)无法和应用线程并发,必须短暂STW,核心原因是写屏障只能拦截「写操作」,无法感知「对象地址变化」:

G1在回收内存时,会将存活对象从垃圾较多的Region转移到空闲Region(压缩内存,减少碎片),此时对象的实际内存地址会发生变化:

  1. 堆中对象A的原地址是0x1001,被GC线程转移到0x2001;

  2. 应用线程的栈/其他对象中,还保存着A的旧地址0x1001;

  3. 写屏障只有在应用线程执行「写操作」时才会触发,如果应用线程执行**「读操作」**(如Object a = A;),直接读取旧地址0x1001,会访问到无效内存(已被回收),引发程序崩溃;

为了避免这个问题,G1在转移对象时,必须暂停所有应用线程(STW),一次性将所有引用旧地址的地方全部修改为新地址,再恢复应用线程——这个过程叫**「引用重定位」**,必须STW执行。

核心结论:写屏障的触发时机是「写操作」,对「读操作」无感知,无法处理对象地址变化后的读操作脏引用问题,因此转移对象必须STW。


二、读屏障:ZGC的核心,「读操作时做GC辅助」 ​

读屏障是ZGC(Z Garbage Collector)的核心创新,也是ZGC能实现**「几乎无STW的并发转移对象」的关键——ZGC彻底摒弃了写屏障的局限性,用读屏障拦截所有引用读操作**,解决了「对象并发转移时的脏引用问题」。

ZGC是面向大内存的低延迟GC(支持TB级堆内存),其核心设计目标是将STW时间控制在10ms以内,无论堆内存多大,而读屏障就是实现这个目标的核心。

1. 读屏障的核心适用场景:GC的「并发转移阶段」 ​

ZGC的所有核心阶段(标记、转移、清理)都能和应用线程完全并发,其中并发转移阶段是读屏障的核心用武之地——解决了G1的痛点:对象地址变化后,应用线程读取脏引用的问题。

ZGC的读屏障采用**「Load Barrier(加载屏障)」**,核心触发时机:应用线程执行任何「引用读取操作」时(如Object a = obj.foo;/if (b != null)/for (Object o : list)),JVM都会自动插入读屏障逻辑。

2. ZGC读屏障的核心设计:「指针染色」+「读屏障重定位」 ​

ZGC能实现并发转移的关键是两个技术的结合,读屏障是其中的执行入口:

前置技术:指针染色(Pointer Coloring) ​

ZGC运行在64位JVM上,利用64位指针的高4位作为**「标志位」(染色),记录对象的状态信息**(无需修改对象本身),核心标志位:

  • 可访问状态:对象正常,地址有效;

  • 转移中状态:对象正在被GC线程转移,旧地址仍可访问,新地址已分配;

  • 转移完成状态:对象已转移完成,旧地址失效,需重定位到新地址。

核心特点:指针的低60位用于存储实际内存地址,高4位做标志,应用线程无需感知,ZGC底层自动处理。

读屏障的核心逻辑:「引用重定位」(Relocation) ​

这是ZGC读屏障的核心工作,也是解决并发转移的关键,所有引用读取操作都会触发这个逻辑,伪代码如下:

java
// 应用线程原逻辑:Object a = obj.foo;
// ZGC插入读屏障后的实际执行逻辑
Object readBarrier(Object* ref) {
// 1. 读取引用的指针,解析标志位和实际地址
Object* ptr = ref;
while (true) {
// 2. 检查指针标志位,判断对象状态
if (ptr的标志位是「可访问」) {
return *ptr;
// 状态正常,直接返回对象
}
if (ptr的标志位是「转移中/转移完成」) {
// 3. 核心:重定位——根据旧地址找到对象的新地址
Object* newPtr = GC线程的转移映射表.get(ptr);
// 4. 原子更新引用为新地址(写回栈/对象中)
CAS(ref, ptr, newPtr);
// 5. 返回新地址的对象
return *newPtr;
}
}
}

通俗解释:应用线程读取引用时,读屏障会先「检查这个引用的对象是否被转移」,如果是,就自动将引用更新为新地址,再返回新地址的对象——整个过程应用线程无感知,且是原子操作(CAS保证),不会出现并发问题。

3. ZGC读屏障的其他作用:辅助并发标记 ​

和写屏障一样,ZGC的读屏障也能保证并发标记的准确性,且逻辑更简单:

GC并发标记时,读屏障会检查读取的对象是否已被标记,若未标记,则先标记该对象,再返回对象——相当于「读操作时顺便完成标记」,无需像CMS/G1那样维护脏队列/SATB队列,简化了GC逻辑。

4. 关键:为什么读屏障能让ZGC并发转移对象? ​

核心原因是读屏障拦截了「所有引用读操作」,实现了**「延迟的引用重定位」**:

  1. GC线程转移对象时,无需暂停应用线程,只需将对象的指针标志位设为「转移中」,并在转移映射表中记录「旧地址→新地址」的映射关系;

  2. 应用线程读取被转移的对象时,读屏障会自动触发,通过转移映射表找到新地址,原子更新引用为新地址,后续该应用线程再访问这个对象时,直接读取新地址,无需再次重定位;

  3. 当所有应用线程的引用都被重定位为新地址后,GC线程再回收旧地址的内存,完成转移。

核心结论:读屏障将**「引用重定位」这个工作,从GC线程的STW阶段,延迟到了应用线程的「读操作」阶段**——也就是「ZGC 过分地把部分垃圾回收工作交给用户线程(应用线程)」。


三、读屏障 vs 写屏障:核心差异对比 ​

用一张表清晰对比二者的核心区别,这是理解CMS/G1/ZGC设计差异的关键:

对比维度写屏障(CMS/G1)读屏障(ZGC)
触发时机应用线程执行引用写操作时(a = b;)应用线程执行引用读操作时(Object a = b;)
核心拦截范围仅拦截写操作,读操作无感知仅拦截读操作,写操作无感知(ZGC无写屏障)
核心作用解决并发标记的漏标问题,辅助跨代引用管理解决并发转移的脏引用问题,辅助并发标记
GC阶段支持仅支持并发标记,转移/清理需STW支持并发标记+并发转移+并发清理,全程几乎无STW
性能开销小(CMS极轻,G1中等),对应用影响可忽略中等(实测最高4%),读操作密集型应用影响稍大
实现复杂度较低(CMS增量更新,G1 SATB)高(依赖指针染色、CAS原子操作、转移映射表)
适用GCCMS、G1、ParallelGC(部分场景)ZGC、Shenandoah(另一个低延迟GC)
核心思想写操作时记录引用变化,GC线程后续补充处理读操作时检查对象状态,实时完成引用重定位

四、补充:ZGC为什么选择读屏障,而不是优化写屏障? ​

ZGC的设计者并非没考虑过写屏障,而是写屏障的先天局限性无法解决并发转移问题,而读屏障是唯一的解决方案:

  1. 触发时机的局限性:写屏障只有写操作时触发,无法处理读操作的脏引用,而应用线程的读操作远多于写操作(大部分业务代码都是读多写少);

  2. 内存开销的局限性:如果用写屏障实现并发转移,需要为所有对象维护「地址映射」,并在写操作时更新,会带来巨大的内存和性能开销;

  3. 64位JVM的天然优势:ZGC依托64位JVM的指针染色技术,让读屏障能快速解析对象状态,无需修改对象本身,这是读屏障能落地的基础。

核心取舍:ZGC用最高4%的应用性能开销,换取了几乎无STW的GC能力——对于大内存应用(如微服务、大数据、中间件),低延迟的价值远大于4%的性能损耗,这也是ZGC成为大内存应用首选GC的原因。


五、最终总结:读/写屏障的核心本质 ​

  1. 二者都是JVM的内存操作拦截器,无感知插入应用线程,核心目的是提升GC的并发能力,减少STW,本质是「用应用线程的少量性能开销,换取GC低延迟」;

  2. 写屏障是「写时记录」,核心解决并发标记的漏标问题,但无法处理对象转移后的脏引用,因此CMS/G1转移对象必须STW;

  3. 读屏障是「读时检查」,核心解决并发转移的脏引用问题,通过指针染色+延迟重定位,让ZGC实现全程并发GC,几乎无STW;

  4. 性能与能力的取舍:写屏障性能开销小,但GC并发能力有限;读屏障性能开销稍大,但GC并发能力拉满,ZGC的选择是「低延迟优先」;

  5. 应用场景:写屏障适用于中小堆内存、对性能损耗敏感的应用(G1是主流);读屏障适用于大堆内存、对延迟要求极高的应用(ZGC是首选)。

一句话概括:

写屏障是「GC的监控器」,记录应用线程的引用变化,让GC能并发标记;

读屏障是「GC的矫正器」,修正应用线程的脏引用,让GC能并发转移。


第二部分:ZGC 并发转移的两个边界问题 ​

核心结论 ​

  1. ZGC不会无限等待应用线程读操作来完成重定位,而是通过GC的「最终重定位阶段(Final Relocation)」,以极短的STW一次性处理所有仍未被应用线程重定位的旧地址引用,彻底保证原Region的所有旧引用都被修正;
  2. 即便某个类/对象长期未被访问,当它最终被读取时,读屏障依然会生效(只要旧Region的转移映射表还在),若映射表已销毁,说明该引用早已被STW阶段修正,绝不会出现空指针——读屏障的生效时机和对象是否长期未访问无关,只和引用读取操作有关。

简单说:ZGC的并发重定位是「应用线程主动读操作时的延迟修正」+「GC线程最终STW的兜底修正」,双重保障让旧地址引用无一漏网,既实现了大部分场景的无STW,又避免了“无限等待”和“空指针”问题。

下面拆解ZGC的具体实现流程和关键机制,把“怎么确定重定位完成”“长期未访问的引用怎么处理”这两个核心点讲透,同时补充ZGC的指针状态设计,让你理解全程无空指针的底层逻辑。

一、先纠正一个认知:ZGC不会“等所有应用线程都读一遍对象”才销毁映射表 ​

你的顾虑核心是“如果有引用一直不读,是不是映射表永远不能销毁,原Region永远不能复用?”——答案是不会,因为ZGC的并发转移分为**「并发重定位阶段」和「最终重定位阶段」**,前者是应用线程的异步延迟修正,后者是GC线程的STW兜底修正,最终重定位阶段会终结所有旧地址的生命周期。

ZGC对单个Region的完整转移流程(核心是解决“旧引用重定位”)分为5步,其中第4步Final Relocation是解决你问题的关键,全程结合指针染色和转移映射表,且STW时间极短(无论堆多大,都在10ms内):

单个Region的完整转移流程(R原:待转移的原Region,R新:目标空闲Region) ​

步骤1:初始标记(Initial Mark)—— 极短STW ​

GC线程先暂停所有应用线程(STW),做3件事:

  1. 标记GC根节点(栈引用、静态变量、JNI引用等)中指向R原的对象;

  2. 将R原的指针状态标记为「转移中(Relocating)」(64位指针高4位染色);

  3. 为R原创建转移映射表(空表),准备记录「旧地址→新地址」的映射;

特点:STW时间极短,仅处理GC根,不扫描整个Region。

步骤2:并发转移(Concurrent Relocation)—— 无STW,GC线程干活 ​

GC线程恢复应用线程,开始批量转移R原中的所有存活对象到R新,每转移一个对象做2件事:

  1. 在R原的转移映射表中记录「该对象的旧地址→新地址」;
  2. 保持对象的指针状态为「转移中(Relocating)」;

特点:GC线程独立干活,应用线程正常运行,此时R原的内存仍可被访问(旧地址有效),不会被回收。

步骤3:并发重定位(Concurrent Remapping)—— 无STW,应用线程兜底 ​

应用线程正常执行,只要读取R原中对象的旧地址,就会触发读屏障,执行「旧地址→新地址」的重定位(CAS更新引用为新地址),这个过程就是你之前理解的“应用线程替GC线程干活”。

核心状态:此时R原的转移映射表存在,指针状态为「转移中」,无论对象多久没被访问,只要第一次读,读屏障必触发重定位,绝不会空指针。

特点:大部分旧引用会在这个阶段被应用线程自动修正,GC线程仅需做少量辅助工作(如统计重定位完成率)。

步骤4:最终重定位(Final Relocation)—— 极短STW,GC兜底所有未修正的旧引用 ​

这是解决“长期未访问的旧引用”的核心步骤,当GC线程判断“R原的转移映射表可以销毁”时(如GC准备进入清理阶段、堆内存不足时),会再次暂停所有应用线程(极短STW),做2件关键事:

  1. 扫描所有GC根节点,找到仍指向R原旧地址的引用,一次性强制重定位为新地址(直接修改栈/静态变量中的引用,无需等待应用线程读操作);
  2. 检查所有仍在使用的线程本地缓存(TLAB),修正其中的旧地址引用;

核心作用:GC根是所有对象引用的“源头”,只要修正了GC根中的旧引用,堆中所有间接的旧引用都会在后续的读操作中被读屏障修正,相当于从源头终结了旧地址的存在。

特点:STW时间极短——仅扫描GC根,不扫描整个堆,而GC根的数量远小于堆中对象数,因此无论堆多大(哪怕TB级),这一步的STW都在10ms内。

步骤5:清理与复用(Concurrent Cleanup)—— 无STW,销毁映射表+释放原Region ​

GC线程恢复应用线程,做3件事:

  1. 将R原的指针状态标记为「转移完成(Relocated)」(染色更新);

  2. 销毁R原的转移映射表(此时已无任何有效旧引用,映射表无用);

  3. 将R原的内存标记为空闲,等待后续对象分配时复用;

关键:此时所有引用都已指向R新的新地址,R原的旧地址彻底失效,且不会有任何新的读操作访问到R原的旧地址——因为GC根中的旧引用已被Final Relocation修正,堆中间接旧引用已被读屏障修正。

流程核心总结:Final Relocation是“兜底神器” ​

ZGC不会依赖应用线程的读操作来完成所有重定位,而是通过Final Relocation的极短STW,从GC根这个源头强制修正所有未被处理的旧引用,彻底解决“长期未访问的引用”问题——这一步是ZGC设计的关键取舍:用极短的STW替代“无限等待”,既保证了低延迟,又保证了内存的正常回收和复用。

二、核心机制1:Final Relocation为什么能“从源头终结旧地址”?—— GC根的特殊性 ​

要理解Final Relocation的兜底作用,必须明确**GC根(GC Roots)**的核心特殊性:堆中所有对象的引用,最终都来源于GC根,没有任何一个堆对象的引用是“无源头的”。

  • 栈中的局部变量引用(如Object a = new Object())→ 属于GC根;

  • 类的静态变量引用(如public static Object b;)→ 属于GC根;

  • JNI中的全局引用 → 属于GC根;

  • 堆中对象A引用对象B,对象B引用对象C → A的引用最终追溯到GC根;

基于这个特性,ZGC在Final Relocation阶段只需修正GC根中的旧引用,就相当于“掐断了所有旧地址的源头”——堆中剩余的间接旧引用(如A→B→旧C),会在应用线程后续读取A/B/C时被读屏障自动修正,且这个过程必然发生在原Region被销毁前。

举例子:长期未访问的静态变量引用

java
// 静态变量,属于GC根,指向R原中的对象X(旧地址0x100000)
public static Object staticObj = X;
// 这个静态变量很久没被访问,应用线程也没触发过读屏障

当R原进入Final Relocation阶段时,GC线程会STW并直接修改这个静态变量,将staticObj的引用从旧地址0x100000更新为新地址0x200000(R新中的X)——此时这个“长期未访问的引用”被强制修正,后续哪怕再过很久,应用线程读取staticObj时,访问的也是新地址,不会触发任何旧地址的读操作。

三、核心机制2:哪怕Final Relocation前读取“长期未访问的引用”,也绝不会空指针——读屏障+转移映射表的双重保障 ​

假设某个引用在Final Relocation阶段前被长期未访问,当它最终被应用线程读取时,为什么不会空指针?——因为此时转移映射表仍存在,指针状态仍为「转移中」,读屏障必然触发并重定位,全程是JVM底层的自动处理,应用线程无感知。

长期未访问引用的读取流程(Final Relocation前) ​

假设:静态变量staticObj指向R原中的对象X(旧地址0x100000),半年未被访问,此时R原处于「转移中」状态,转移映射表存在,X已被转移到R新(新地址0x200000),映射表记录0x100000→0x200000。

当应用线程执行Object a = staticObj;(第一次读取这个长期未访问的引用)时,流程如下:

  1. 应用线程读取staticObj的旧地址0x100000,触发读屏障(只要是引用读取,必触发,和是否长期未访问无关);

  2. 读屏障解析指针高4位,发现是「转移中」状态,确定需要重定位;

  3. 读屏障通过旧地址解析出所属R原,找到仍存在的转移映射表,查得新地址0x200000;

  4. 读屏障通过CAS原子操作,将staticObj的引用从旧地址更新为新地址(后续读取直接用新地址,无需再重定位);

  5. 读屏障返回新地址0x200000指向的对象X,应用线程正常执行,无任何空指针;

核心:读屏障的触发条件是「引用读取操作」,和对象/引用是否被长期访问无关——只要你读,就触发,只要触发,就会检查并修正旧地址,而Final Relocation前转移映射表一定存在,因此绝不会空指针。

四、核心机制3:Final Relocation后,旧地址引用已不存在,原Region销毁无风险 ​

当R原完成Final Relocation阶段后,所有GC根中的旧地址引用都被强制修正,此时堆中可能存在的仅有的间接旧地址引用,会在应用线程后续第一次读取时被读屏障修正,且这个修正过程发生在转移映射表销毁前。

而当GC线程执行**步骤5(清理与复用)**时,会先确认:

  1. GC根中已无任何R原的旧地址引用;
  2. 所有间接旧地址引用的修正窗口已过(应用线程后续读取会自动修正);

此时销毁转移映射表、将R原标记为空闲,不会有任何风险——因为后续所有引用读取的都是新地址,不会再访问到R原的旧地址,自然不会出现“读已销毁Region的空指针”问题。

五、补充:ZGC的指针状态设计,从底层杜绝空指针 ​

ZGC的64位指针高4位的染色状态是“无空指针”的底层保障,这4个状态的切换顺序是严格可控的,且任何状态下的指针读取,读屏障都能处理,绝不会出现“指针指向无效内存”的情况。

ZGC的核心指针状态(高4位):

  1. 可访问(Marked0/Marked1):对象正常,地址有效,读屏障直接返回对象;

  2. 转移中(Relocating):对象正在被转移,旧地址有效,转移映射表存在,读屏障会重定位;

  3. 转移完成(Relocated):对象已转移,旧地址失效,但GC根已修正,间接旧引用会被读屏障最后一次修正;

状态切换的核心规则:

  • 只有进入转移中状态,才会创建转移映射表;

  • 只有完成Final Relocation,才会切换为转移完成状态;

  • 只有切换为转移完成状态,才会销毁转移映射表;

  • 任何状态下的指针,都不会指向“已被回收的无效内存”——转移中时旧地址有效,转移完成时旧地址已无引用访问。

这个状态设计,从底层保证了无论对象处于哪个阶段,应用线程的读操作都能被读屏障正确处理,绝不会出现空指针异常。

最终总结:ZGC解决“无限等待”和“空指针”的核心逻辑 ​

ZGC之所以能既实现几乎无STW的并发转移,又避免“等所有应用线程读操作”和“长期未访问引用空指针”问题,核心是3个机制的结合,也是ZGC设计的精髓:

  1. 延迟重定位:让应用线程在读操作时异步修正旧地址,实现大部分场景的无STW,减少GC的STW时间;

  2. Final Relocation兜底:通过极短的STW扫描GC根,强制修正所有未被应用线程处理的旧引用,从源头终结旧地址的生命周期,避免无限等待;

  3. 指针染色+转移映射表:读屏障结合指针状态,无论对象是否长期未访问,只要读就触发重定位,且转移映射表在Final Relocation前始终存在,从底层杜绝空指针;

而你担心的“有个类好久没用读数据了”,ZGC的处理方式是:

  • 若在Final Relocation前读取:读屏障触发,通过转移映射表重定位为新地址,无空指针;
  • 若在Final Relocation后读取:该引用早已被GC根强制修正为新地址,直接访问新对象,无空指针;

这也是ZGC能成为大内存低延迟GC的核心原因——它把“引用重定位”的工作合理分配给应用线程(并发)和GC线程(STW兜底),既利用了应用线程的空闲资源,又通过极短的STW保证了GC的完整性,实现了“性能”和“低延迟”的最优解。

贡献者 ​

文件历史 ​