18ZGC和ShenandoahGC原理

1.ZGC原理

ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到16TB 的堆大小,堆大小对STW的时间基本没有影响。

在G1垃圾回收器中,STW时间的主要来源是在转移阶段:

1、初始标记,STW,采用三色标记法标记从GC Root可直达的对象。 STW时间极短

2、并发标记,并发执行,对存活对象进行标记。

3、最终标记,STW,处理SATB相关的对象标记。 STW时间极短

4、清理,STW,如果区域中没有任何存活对象就直接清理。 STW时间极短5、转移,将存活对象复制到别的区域。 STW时间较长

1.1G1转移时需要停顿的主要原因

在转移时,能不能让用户线程和GC线程同时工作呢?考虑下面的问题:

转移完之后,需要将A对对象的引用更改为新对象的引用。但是在更改前,执行A.c.count = 2,此时更改的是转移前对象中的属性

更改引用之后, A引用了转移之后的对象,此时获取A.c.count发现属性值依然是1。这样就产生了问题,所以G1为了解决问题,在转移过程中需要进行用户线程的停止。ZGC和Shenandoah解决了这个问题,让转移过程也能够并发执行。

在ZGC中,使用了读屏障Load Barrier技术,来实现转移后对象的获取。当获取一个对象引用时,会触发读后的屏障指令,如果对象指向的不是转移后的对象,用户线程会将引用指向转移后的对象。

f变量一开始指向转移前的对象:

通过读后屏障指令,判断如果是转移前的对象,就改写指针内容,指向转移后的对象。

这样对f.count进行赋值操作,操作的就是转移后的对象了:

那么ZGC是如何判断对象是转移前还是转移后的呢?它主要使用了着色指针(Colored Pointers)

1.2着色指针(Colored Pointers)

着色指针将原来的8字节保存地址的指针拆分成了三部分:

1、最低的44位,用于表示对象的地址,所以最多能表示16TB的内存空间。

2、中间4位是颜色位,每一位只能存放0或者1,并且同一时间只有其中一位是1。

终结位:只能通过终结器访问

重映射位(Remap):转移完之后,对象的引用关系已经完成变更。

Marked0和Marked1:标记可达对象

3、16位未使用

访问对象引用时,使用的是对象的地址。在64位虚拟机中,是8个字节可以表示接近无限的内存空间。所以一般内存中对象,高几位都是0没有使用。着色指针就是利用了这多余的几位,存储了状态信息。

正常应用程序使用8个字节去进行对象的访问,现在只使用了44位,不会产生问题吗?

应用程序使用的对象地址,只是虚拟内存,操作系统会将虚拟内存转换成物理内存。而ZGC通过操作系统更改了这层逻辑。所以不管颜色位变成多少,指针指向的都是同一个对象。

在ZGC中,与G1垃圾回收器一样将堆内存划分成很多个区域,这些内存区域被称之为Zpage。

Zpage分成三类大中小,管控粒度比G1更细,这样更容易去控制停顿时间。

小区域:2M,只能保存256KB内的对象。

中区域:32M,保存256KB – 4M的对象。

大区域:只保存一个大于4M的对象。

1.4初始标记阶段

标记Gc Roots引用的对象为存活对象数量不多,所以停顿时间非常短。

初始阶段会标记GC Roots直接关联的对象,对引用这些对象的指针上的marked0位标记为1:

1.4并发标记阶段

遍历所有对象,标记可以到达的每一个对象是否存活,用户线程使用读屏障,如果发现对象没有完成标记也会帮忙进行标记。

1.4并发处理阶段

选择需要转移的Zpage,并创建转移表,用于记录转移前对象和转移后对象地址。

1.4转移开始阶段

转移GC Root直接关联的对象,不转移的对象remapped值设置成1,避免重复进行判断。

如下1和2不转移,将remapped置为1:

接下来开始转移:

1.4并发转移阶段

将剩余对象转移到新的ZPage中,转移之后将两个对象的地址记入转移映射表。

转移完之后,转移前的Zpage就可以清空了,转移表需要保留下来。

此时,如果用户线程访问4对象引用的5对象,会通过读屏障,将4对5的引用进行重置,修改为对5的引用,同时将remap标记为1代表已经重新映射完成。

并发转移阶段结束之后,这一轮的垃圾回收就结束了,但其实并没有完成所有指针的重映射工作,这个工作会放到下一阶段,与下一阶段的标记阶段一起完成(因为都需要遍历整个对象图)。

1.4第二次垃圾回收的初始标记阶段

第二次垃圾回收的初始标记阶段,沿着GC Root标记对象。这一次会使用marked1,因为marked0是上一次垃圾回收了。这样可以很容易区分出是这一次垃圾回收的标记阶段还是上一次垃圾回收的。

如果Marked0为1代表上一轮的重映射还没有完成,先完成重映射从转移表中找到老对象转移后的新对象,再进行标记。如果Remap为1,只需要进行标记。

将转移映射表删除,释放内存空间。

1.5并发问题

如果用户线程在帮忙转移时,GC线程也发现这个对象需要复制,那么就会去尝试写入转移映射表,如果发现映射表中已经有相同的老对象,直接放弃。

1.6分代ZGC的设计

在JDK21之后,ZGC设计了年轻代和老年代,这样可以让大部分对象在年轻代回收,减少老年代的扫描次数,同样可以提升一定的性能。同时,年轻代和老年代的垃圾回收可以并行执行。

分代之后的着色指针将原来的8字节保存地址的指针拆分成了三部分:

1、46位用来表示对象地址,最多可以表示64TB的地址空间。

2、中间的12位为颜色位。

3、最低4位和最高2位未使用

整个分代之后的读写屏障、着色指针的移位使用都变的异常复杂,仅作了解即可。

2.ShenandoahGC原理

ShenandoahGC和ZGC不同, ShenandoahGC很多是使用了G1源代码改造而成,所以在很多算法、数据结构的定义上,与G1十分相像,而ZGC是完全重新开发的一套内容。

1、ShenandoahGC的区域定义与G1是一样的。

2、没有着色指针,通过修改对象头的设计来完成并发转移过程的实现

3、ShenandoahGC有两个版本,1.0版本存在于JDK8和JDK11中,后续的JDK版本中均使用2.0版本。

2.1 ShenandoahGC1.0版本

如果转移阶段未完成,此时转移前的对象和转移后的对象都会存活。如果用户去访问数据,需要使用转移后的数据。 ShenandoahGC使用了读前屏障,根据对象的前向指针来获取到转移后的对象并读取。

写入数据时会使用写前屏障,判断Mark Word中的GC状态,如果GC状态为0证明没有处于GC过程中,直接写入,如果不为0则根据GC状态值确认当前处于垃圾回收的哪个阶段,让用户线程执行垃圾回收相关的任务。

1.0版本的缺点:

1、对象内存大大增加,每个对象都需要增加8个字节的前向指针,基本上会占用5% – 10%的空间。

2、读屏障中加入了复杂的指令,影响使用效率。

2.2 ShenandoahGC2.0版本

2.0版本优化了前向指针的位置,仅转移阶段将其放入了Mark Word中。

2.3 ShenandoahGC的执行流程

2.4 并发转移阶段 – 并发问题

如果用户线程在帮忙转移时,ShenandoahGC线程也发现这个对象需要复制,那么就会去尝试写入前向指针,使用了类似CAS的方式来实现,只有一个线程能成功修改,其他线程会放弃转移的操作。

9 条评论

  1. Edith4890 2025-11-30 19:31 回复
  2. Kirk2025 2025-12-02 10:22 回复
  3. Katelyn1421 2025-12-17 19:52 回复
  4. Chuck491 2025-12-28 21:47 回复
  5. Samantha413 2026-01-05 07:05 回复

回复 Noelle2969 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注