如果是的话修正对象的引用,按照上面的例子,不仅 a 能得到最新的引用地址,obj.foo 也会被更新,这样下次访问的时候一切都是正常的,就没有消耗了。
下图展示了读屏障的效果,其实就是转移的时候找地方记一下即 forwardingTable,然后读的时候触发引用的修正。
这种也称之为“自愈”,不仅赋值的引用时最新的,自身引用也修正了。
染色指针和读屏障是 ZGC 能实现并发转移的关键所在。
ZGC 回收流程解析ZGC 的步骤大致可分为三大阶段分别是标记、转移、重定位。
标记:从根开始标记所有存活对象
转移:选择部分活跃对象转移到新的内存空间上
重定位:因为对象地址变了,所以之前指向老对象的指针都要换到新对象地址上。
并且这三个阶段都是并发的。
这是意识上的阶段,具体的实现上重定位其实是糅合在标记阶段的。
在标记的时候如果发现引用的还是老的地址则会修正成新的地址,然后再进行标记。
简单的说就是从第一个 GC 开始经历了标记,然后转移了对象,这个时候不会重定位,只会记录对象都转移到哪里了。
在第二个 GC 开始标记的时候发现这个对象是被转移了,然后发现引用还是老的,则进行重定位,即修改成新的引用。
所以说重定位是糅合在下一步的标记阶段中。
我再简单说一下十个步骤。
不过步骤里有些不影响整体回收流程的,我就不多加分析了。
这篇文章的目的不是深入 ZGC 实现的细节,而是了解 ZGC 大致的突出点和简单流程即可。
因此想知道细节的自行查阅,或者可以看看我文末推荐的书籍。
初始标记这个阶段其实大家应该很熟悉,CMS、G1 都有这个阶段,这个阶段是 STW 的,仅标记根直接可达的对象,压到标记栈中。
当然还有其他动作,比如重置 TLAB、判断是否要清除软引用等等,不做具体分析。
并发标记就是根据初始标记的对象开始并发遍历对象图,还会统计每个 region 的存活对象的数量。
这个并发标记其实有个细节,标记栈其实只有一个,但是并发标记的线程有多个。
为了减少之间的竞争每个线程其实会分到不同的标记带来执行。
你就理解为标记栈被分割为好几块,每个线程负责其中的一块进行遍历标记对象,就和1.7 Hashmap 的segment 一样。
那肯定有的线程标记的快,有的标记的慢,那么先空闲下来的线程会去窃取别人的任务来执行,从而实现负载均衡。
看到这有没有想到啥?没错就是 ForkJoinPool 的工作窃取机制!
再标记阶段这一阶段是 STW 的,因为并发阶段应用线程还是在运行的,所以会修改对象的引用导致漏标的情况。
因此需要个再标记阶段来标记漏标的那些对象。
如果这个阶段执行的时间过长,就会再次进入到并发标记阶段,因为 ZGC 的目标就是低延迟,所以一有高延迟的苗头就得扼制。
这个阶段还会做非强根并行标记,非强根指的是:系统字典、JVMTI、JFR、字符串表。
有些非强根可以并发,有些不行,具体不做分析。
非强引用并发标记和引用并发处理就是上一步非强根的遍历,然后引用就软引用、弱引用、虚引用的一些处理。
这个阶段是并发的。
重置转移集