ZGC 详解

目录

  • ZGC简介和性能
  • ZGC流程介绍
  • ZGC堆的内存布局
  • ZGC对NUMA支持
  • 颜色指针在ZGC中的运用
  • 读屏障在ZGC中的运用
  • strip(条带)在ZGC中的运用
  • ZGC全流程动画示意

一, ZGC简介和性能

G1的目标是在可控的停顿时间内完成垃圾回收,所以进行了分区设计,在回收时采用部分内存回收(在YGC时会回收所有新生代分区,在混合回收时会回收所有的新生代分区和部分老生代分区),支持的内存也可以达到几十个GB或者上百个GB。为了进行部分回收,G1实现了RSet管理对象的引用关系。基于G1设计上的特点,导致存在以下问题:

  1. 停顿时间过长,通常G1的停顿时间要达到几十到几百毫秒;这个数字其实已经非常小了,但是我们知道垃圾回收发生导致应用程序在这几十或者几百毫秒中不能提供服务,在某些场景中,特别是对用户体验有较高要求的情况下不能满足实际需求。
  2. 内存利用率不高,通常引用关系的处理需要额外消耗内存,一般占整个内存的1%~20%左右。
  3. 支持的内存空间有限,不适用于超大内存的系统,特别是在内存容量高于100GB的系统中,会因内存过大而导致停顿时间增长。

ZGC作为新一代的垃圾回收器,在设计之初就定义了三大目标:

  1. 支持TB级内存
  2. 停顿时间控制在10ms之内
  3. 对程序吞吐量影响小于15%。


    image.png

实际上目前ZGC已经满足设计之初定义的目标,最大支持4TB堆空间,依据实际测试的情况来看,停顿时间通常都在10ms以下,并且垃圾回收所引起的暂停时间并不会随着内存的增大而延长。

ZGC如何设计以达成目标?
简单地说,就是ZGC把一切能并发处理的工作都并发执行。
ZGC是在G1的基础上发展起来的,我们知道G1中实现了并发标记,所以标记已经不会再影响停顿时间了。

G1中的停顿时间主要来自垃圾回收(YGC和混合回收)阶段中的复制算法,在复制算法中,需要把对象转移到新的空间中,并且更新其他对象到这个对象的引用。实际中对象的转移涉及内存的分配和对象成员变量的复制,而对象成员变量的复制是非常耗时的。

在G1中对象的转移都是在STW中并行执行的,而ZGC就是把对象的转移也并发执行,从而满足停顿时间在10ms以下。

我们看到G1只有在MARKING的时候,是并发的。而ZGC 在对象的复制和压缩,复制集的选择,等很多方面都改成了并发(和应用线程同时进行)。这就是它STW时间如此之短的秘诀。


image.png

在最新的JDK版本中,后面2项没有打钩的,ZGC也把他们优化进并发步骤中,根据他们最新的测试,STW最大停顿时间可以缩小到1MS。

上图左边也展示了一些ZGC里的经典特性。比如说用到了读屏障,颜色指针。是个单代的垃圾收集器(不区分年轻代和老年代, 在JVMLS 2018上,ZGC的领队Per大大明确表示目前的ZGC没有分代只是为了实现简单,目前正在考虑给ZGC添加分代支持或者是添加一个Thread-Local GC来起到类似Young GC的作用,还在探索中),是个部分压缩的算法(和G1类似,有解决内存碎片的标记整理步骤)。立即的内存重用,和NUMA友好的特性。
我们会在下文中或多或少的展开介绍这些特点。
在JDK11中的ZGC只支持 Linux/x86_64


image.png

现在ZGC的目标是1MS,原来ZGC的停顿时间和堆SIZE无关,但是和ROOT-SET的SIZE有关。他们希望实现并发的线程栈扫描,这样可以使得ZGC的停顿时间和ROOT-SET SIZE也无关。可以把性能提升到1MS的效果。大概和下图这么牛逼


image.png

当然我们再来看看ZGC和G1比有多牛逼。


image.png
image.png

在缩短延迟的同时,吞吐率也不受影响。


image.png

image.png

一, ZGC流程介绍

并发垃圾回收算法实际上是以复制算法为基础,增加了并发处理。我们先回顾一下复制算法,它可以概括为3个阶段,分别为标记(mark)、转移(relocate)和重定位(remap)。这3个阶段分别完成的功能是:

  • 标记:从根集合出发,标记活跃对象;此时内存中存在活跃对象和已死亡对象。
  • 转移:把活跃对象转移(复制)到新的内存上,原来的内存空间可以回收。
  • 重定位:因为对象的内存地址发生了变化,所以所有指向对象老地址的指针都要调整到对象新的地址上。

从细节角度可以分为如下步骤


image.png

1)初始标记,从根集合出发,找出根集合直接引用的活跃对象,并入栈;该步需要STW。
2)并发标记,根据初始标记找到的根对象,使用深度优先遍历对象的成员变量进行标记;并发标记需要解决标记过程中引用关系变化导致的漏标记问题
3)再标记和非强根并行标记,在并发标记结束后尝试终结标记动作,理论上并发标记结束后所有待标记的对象会全部完成,但是因为GC工作线程和应用程序线程是并发运行,所以可能存在GC工作线程执行结束标记时,应用程序线程又有新的引用关系变化导致漏标记,所以这一步先判断是否真的结束了对象的标记,如果没有结束就还会启动并行标记,所以这一步需要STW。另外,在该步中,还会对非强根(软应用,虚引用等)进行并行标记。
4)并发处理非强引用和非强根并发标记
5)重置转移集合中的页面,实际上第一次垃圾回收时无须处理这一步。
6)回收无效的页面,实际上在内存充足的情况下不会触发这一步。
7)并发选择对象的转移集合,转移集合中就是待回收的页面。
8)并发初始化转移集合中的每个页面,在后续重定位(也称为Remap)时需要的对象转移表(Forward Table)就是在这一步初始化的。
9)转移根对象引用的对象,该步需要STW。
10)并发转移,把对象移动到新的页面中,这样对象所在的老的页面中所有活跃对象都被转移了,页面就可以被回收重用。

image.png

为了画图方便,把步骤5)~步骤8)放在一个并发步骤中,实际中这是4步,并且这4步是串行执行,每一步都是并发执行的。

上述步骤,你可能会看晕。我们来看简单的版本。


image.png

上述3个蓝线代表需要STW, 灰线代表可以并发运作。

  1. 根集合标记(STW)
  2. 并发标记
  3. 并发标记的同步点(STW,同G1)还会处理一些非强根
  4. 并发-转移前的准备(上面的4-8步,最核心的是引用处理,非强根清理,转移集(relocation set)的选择)
  5. 转移 在转移集中的根对象(STW)
  6. 并发转移其他在转移集中的对象

并发算法中明确地提到重定位阶段,但上面的步骤中并没有体现。在ZGC中并没有明确这一步,重定位实际上被合并到标记阶段中,即在标记的时候如果发现对象引用到老的地址,这时会先完成重定位更新对象的引用关系,然后再标记对象。所以实质上ZGC的并发垃圾回收中还是包含了重定位这一阶段,只不过重定位和标记阶段复用了。

image.png

思考题. 有3个步骤需要STW的原因是什么?他们和应用线程并发有什么问题?

第二个STW,如果你看过我的G1的文章,就很好理解。如果不STW,那么应用程序一直在改引用,会找不到一个时间点,把待标记对象在QUEUE中给全部排干净。那么会一直处于并发标记阶段。
第三个STW,在初始转移中所做的工作主要针对根集合引用的对象,如果这些对象所在的页面在转移集中,则转移这些对象;如果对象所在的页面不在转移集中,则直接调整对象的页面映射视图。第10步中的并发标记是对所有在转移集的页面中所有活跃对象做转移,在并发转移之后的下一次垃圾回收的标记阶段完成重定位。那么我们能不能把第9步的工作分散在第10步和下一次垃圾回收的标记阶段进行?
要回答这个问题,需要理解读屏障,和对ZGC的全貌有一个认识,我会放在文章最后解答。
关于第一个STW,也要对ZGC的工作原理有一定的理解。我会在讲完(颜色指针怎么工作后解答这个问题)

三, ZGC堆的内存布局

ZGC 支持的最大堆内存为4T。 但是我们确需要保留更大的虚拟地址空间。如下图


image.png

因为ZGC里需要用到3个地址视图。
分别是Marked0、Marked1和Remapped,而且有趣的是这3个视图会映射到操作系统的同一物理地址。这就是ZGC中Color Pointers的概念,通过Color Pointers来区别不同的虚拟视图。
在ZGC中常见的几个虚拟空间有[0 ~ 4TB)、[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB)。其中[0 ~ 4TB)对应的是Java的堆空间;[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB)分别对应Marked0、Marked1和Remapped这3个视图。这几个区域有什么关系?我们先看图
mutator 就是应用线程的意思


image.png

0~4TB的虚拟地址是ZGC提供给应用程序使用的虚拟空间,它并不会映射到真正的物理地址。
·操作系统管理的虚拟内存为Marked0、Marked1和Remapped这3个空间,且它们对应同一物理空间。

·在ZGC中这3个空间在同一时间点有且仅有一个空间有效。为什么这么设计?这是利用虚拟空间换时间;这3个空间的切换是由垃圾回收的不同阶段触发的。(下文介绍颜色指针会介绍)

应用程序可见并使用的虚拟地址为0~4TB,经ZGC转化,真正使用的虚拟地址为[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB),操作系统管理的虚拟地址也是[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB)。应用程序可见的虚拟地址[0 ~ 4TB)和物理内存直接的关联由ZGC来管理。

image.png

为了细粒度地控制内存的分配,和G1一样,ZGC将内存划分成小的分区,在ZGC中称为页面(page)。ZGC支持3种页面,分别为小页面、中页面和大页面。其中小页面指的是2MB的页面空间,中页面指32MB的页面空间,大页面指受操作系统控制的大页。我们先回顾一下操作系统所支持的大页。

标准大页(huge page)是Linux Kernel 2.6引入的,目的是通过使用大页内存来取代传统的4KB内存页面,以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。它有两种大小:2MB和1GB。2MB页块大小适合用于吉字节级的内存,1GB页块大小适合用于太字节级别的内存;2MB是默认的大页尺寸。

一个ZGC的页面可能由几个不连续的操作系统页面组成。

image.png

ZGC对于不同页面回收的策略也不同。简单地说,小页面优先回收;中页面和大页面则尽量不回收。


image.png

四, ZGC对NUMA支持

在过去,对于X86架构的计算机,内存控制器还没有整合进CPU,所有对内存的访问都需要通过北桥芯片来完成。X86系统中的所有内存都可以通过CPU进行同等访问。任何CPU访问任何内存的速度是一致的,不必考虑不同内存地址之间的差异,这称为“统一内存访问”(Uniform Memory Access,UMA)。UMA系统的架构示意图如图所示。


image.png

在UMA中,各处理器与内存单元通过互联总线进行连接,各个CPU之间没有主从关系。之后的X86平台经历了一场从“拼频率”到“拼核心数”的转变,越来越多的核心被尽可能地塞进了同一块芯片上,各个核心对于内存带宽的争抢访问成为瓶颈,所以人们希望能够把CPU和内存集成在一个单元上(称为Socket),这就是非统一内存访问(Non-Uniform Memory Access,NUMA)。很明显,在NUMA下,CPU访问本地存储器的速度比访问非本地存储器快一些。下图所示是初期处理器架构示意图。


image.png

ZGC是支持NUMA的,在进行小页面分配时会优先从本地内存分配,当不能分配时才会从远端的内存分配。对于中页面和大页面的分配,ZGC并没有要求从本地内存分配,而是直接交给操作系统,由操作系统找到一块能满足ZGC页面的空间。
ZGC这样设计的目的在于,对于小页面,存放的都是小对象,从本地内存分配速度很快,且不会造成内存使用的不平衡,而中页面和大页面因为需要的空间大,如果也优先从本地内存分配,极易造成内存使用不均衡,反而影响性能。

五, 颜色指针在ZGC中的运用

image.png

颜色指针可以说是ZGC的核心概念。因为他在指针中借了几个位出来做事情,所以它必须要求在64位的机器上才可以工作。并且因为要求64位的指针,也就不能支持压缩指针。(关于JVM的压缩指针,可以自行百度其他文章)

ZGC支持64位系统,我们看一下ZGC是如何使用64位地址的。ZGC中低42位(第0 ~ 41位)用于描述真正的虚拟地址(这就是上面提到的应用程序可以使用的堆空间),接着的4位(第42 ~ 45位)用于描述元数据,其实就是大家所说的Color Pointers,还有1位(第46位)目前暂时没有使用,最高17位(第47~63位)固定为0


image.png
image.png

由于42位地址最大的寻址空间就是4TB,这就是ZGC一直宣称自己最大支持4TB内存的原因。这里还有视图的概念,Marked0、Marked1和Remapped就是3个视图,分别将第42、43、44位设置为1,就表示采用对应的视图。在ZGC中,这4位标记位的目的并不是用于地址寻址的,而是为了区分Marked0、Marked1和Remapped这3个视图。当然对于操作系统来说,这4位标记位代表了不同的虚拟地址空间,而这些不同标记位指示的不同虚拟空间通过mmap映射在同一物理地址;颜色指针能够快速实现并发标记、转移和重定位。

为什么最高位16个不能用?

由于X86_64处理器硬件的限制,目前X86_64处理器地址线只有48条,也就是说64位系统支持的地址空间为256TB。为什么处理器的指令集是64位的,但是硬件仅支持48位的地址?最主要的原因是成本问题,即便到目前为止由48位地址访问的256TB的内存空间也是非常巨大的,也没有多少系统有这么大的内存,所以在设计CPU时仅仅支持48位地址,可以少用很多硬件

ZGC基于颜色指针的并发处理算法

ZGC初始化之后,整个内存空间的地址视图被设置为Remapped,当进入标记阶段时的视图转变为Marked0(也称为M0)或者Marked1(也称为M1),从标记阶段结束进入转移阶段时的视图再次设置为Remapped。ZGC通过视图的切换加上SATB算法实现并发处理。具体算法如下。
1.初始化阶段
在ZGC初始化之后,此时地址视图为Remapped,程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动
2.标记阶段
第一次进入标记阶段时视图为M0,在标记阶段,应用程序和标记线程并发执行,那么对象的访问可能来自标记线程和应用程序线程。

  • 标记线程:它从根集合开始标记对象,在标记前先判断对象的地址视图,如果发现对象的地址视图是M0,说明对象是在进入标记阶段之后新分配的对象或者对象已经完成了标记(对象活跃),无须处理。如果发现对象的地址视图是Remapped,说明对象是前一阶段分配的,而且通过根集合可达,所以把对象的地址视图从Remapped调整为M0。(M0表示活跃)
  • 应用程序线程如果创建新的对象,则对象的地址视图为M0。
    如果应用程序线程访问对象并且对象的地址视图是Remapped,说明对象是前一阶段分配的,按照SATB的算法,只要把该对象的视图调整为M0就能防止对象漏标。只标记应用线程访问到的对象还不够,实际上还需要把对象的成员变量所引用的对象都进行递归标记。如果应用线程访问对象地址视图是M0,说明对象是在进入标记阶段之后新分配的对象或者对象已经完成了标记,无须额外处理,直接访问。

所以,在标记阶段结束之后,对象的地址视图要么是M0(活跃),要么是Remapped(垃圾)。这里的虚拟地址虽然不一样,但是指向的是物理内存的同一个区域
所有标记为M0的对象放入活跃信息表

3.并发转移阶段
标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。

转移阶段会把活跃对象转移到新的内存中,并回收对象转移前的内存空间。在转移阶段,应用程序和标记线程并发执行,那么对象的访问可能来自转移线程和应用程序线程。

  • 转移线程:转移线程仅仅根据活跃对象进行转移。当转移线程访问对象时:
    如果对象在对象活跃信息表中并且视图为M0,则转移对象,并且视图从M0调整为Remapped。
    如果对象在对象活跃信息表中并且视图Remapped,说明对象已经被转移,无须处理。

  • 应用程序线程如果创建新的对象,则对象的地址视图为Remapped。
    如果应用线程访问对象且不在活跃信息表中,则说明是新创建的或者对象无须转移,无须处理。
    如果应用线程访问对象且在活跃信息表中且视图为Remapped,说明对象已经被转移,无须处理。
    如果应用程序线程访问在对象活跃信息表中,且视图为M0,说明对象是标记阶段标记的活跃对象,所以需要转移对象
    在对象转移以后,对象的地址视图从M0调整为Remapped;
    注意,只把应用线程读到的对象进行转移还不够,实际上还需要把对象的成员变量所引用的对象都进行转移,ZGC对这一实现做了优化,由转移线程完成对象成员变量的转移。
    至此,ZGC的一个垃圾回收周期中,并发标记和并发转移就结束了。

我们提到在标记阶段存在两个地址视图M0和M1,上面的算法过程显示只用到了一个地址视图,为什么设计成两个?简单地说是为了区别前一次标记和当前标记。

第一次垃圾回收时地址视图为M0,假设标记了两个对象ObjA和ObjB,说明ObjA和ObjB都是活跃的,它们的地址视图都是M0。在转移阶段,ZGC是按照页面进行部分内存垃圾回收的,也就是说当对象所在的页面需要回收时,页面里面的对象需要被转移,如果页面不需要转移,页面里面的对象也就不需要转移。

假设ObjA所在的页面被回收,ObjB所在的页面在这一次垃圾回收中不会被回收。ObjA被转移后,它的地址视图从M0调整为Remapped,ObjB不会被转移,ObjB的地址视图仍然为M0。

那么下一次垃圾回收标记阶段开始的时候,存在两种地址视图的对象

  1. 地址视图为Remapped的对象,说明该对象在并发转移阶段被转移或者被访问过;
  2. 地址视图为M0的对象,说明该对象在前一次垃圾回收的标记阶段已经被标记。

如果本次垃圾回收标记阶段仍然使用M0这个地址视图,那么就不能区分出对象是活跃的,还是上一次垃圾回收标记过的。

所以新标记阶段使用了另外一个地址视图M1,则标记结束后所有活跃对象的地址视图都为M1。
此时这3个地址视图代表的含义是:

  • M1:本次垃圾回收中识别的活跃对象。
  • M0:前一次垃圾回收的标记阶段被标记过的活跃对象,对象在转移阶段未被转移,但是在本次垃圾回收中被识别为不活跃对象。
  • Remapped:前一次垃圾回收的转移阶段发生转移的对象或者是被应用程序线程访问的对象,但是在本次垃圾回收中被识别为不活跃对象。

上述过程算法演示

image.png
image.png

image.png

image.png

六, 读屏障在ZGC中的运用

image.png

image.png

image.png

image.png

对应用程序线程进行标记发生在读对象时,为了触发标记动作可以设计一个读屏障,在字节码层面或者编译代码层面给读操作增加一个额外的处理即可。

读屏障由读命令触发,JVM有3种运行状态:解释执行、C1优化执行和C2优化执行。不同的运行状态,读屏障的触发代码略有不同,但它们使用的读屏障是完全一样的。

我们从最简单的解释执行看一下读屏障的实现。读屏障在解释执行时通过load相关的字节码指令加载数据,我们直接从堆空间中加载对象的地方了解一下读屏障,其代码如下:

template <DecoratorSet decorators, typename BarrierSetT>
template <typename T>
inline oop ZBarrierSet::AccessBarrier<decorators,BarrierSetT>::oop_load_in_heap(T* addr) {
  verify_decorators_absent<ON_UNKNOWN_OOP_REF>();
  const oop o = Raw::oop_load_in_heap(addr);
  return load_barrier_on_oop_field_preloaded(addr, o);
}

这里调用的load_barrier_on_oop_field_preloaded就是读屏障,在对象加载完成后做额外的处理。


image.png

读屏障增加了额外的代码,所以会引起性能下降。据Per Linda的介绍,SPECjbb测试表明使用读屏障之后,性能大概降低4%左右。

image.png

上面提及了标记的一些基本概念,STW的时候是并行的(多个标记线程一起标记ROOT直接引用的对象),之后是并发的。读屏障可以让应用线程再使用对象的时候,知道它是不是一个GOOD COLOR的对象,如果不是。就先要帮他恢复成GOOD COLOR。
如果在标记的时刻,地址视图为M0,发现他不是M0,那么应用线程会帮助把它(如果需要转移还会帮助转移)标记为M0,因为我可以访问到它所以它是活的。
上述的做法通过任意一次读操作,都把颜色置为灰色(活跃),从而打破并发标记中漏标的充要条件(充要条件参考我的G1 详解

回答思考题3

为什么初始标记需要STW?
假如我们不STW,那么应用程序和标记线程并发。CPU可以在任意位置切换。假设现在是初始状态,地址视图为REMAPPED。应用线程从栈中的引用读到一个堆中的对象OBJ_A。并且要把它的引用给断开。

local1.next = obj_a

按照STAB的设计,如果此时已经开始并发标记了,那么OBJ_A会标记为M0。但是此时还没开始。所以应用程序把OBj_A读出来。准备给LOCAL1.NEXT赋值前,切换到了标记线程。
标记线程启动,把地址视图改为M0。然后从LOCAL1这个跟开始扫描。发现LOCAL1.NEXT=NULL。那么把LOCAL1标记为M0就结束了。
切回到应用线程,此时继续完成赋值任务。就造成了一个黑色对象指向了一个白色对象的漏标问题。白色对象会被GC掉之后,程序会出错。
为了解决这个问题,程序会利用STW的安全点,来防止这种应用线程做到一半的操作被切走的情况。

七, strip(条带)在ZGC中的运用

ZGC中引入了标记条带(mark strip)。为了让线程之间标记的时候可以互不干扰,减少竞争锁的开销。


image.png

标记栈在ZGC中的底层实现使用的是数组,因为标记栈只有一个,所有的并发标记线程会访问这一个标记栈,所以自然会想到将这个标记栈进行划分,划分后形成多个标记条带,然后让多个并发标记线程并行地访问其中的标记条带,互不干扰。例如,Thread0标记Strip0中的对象,而Strip0可能包含多个内存区域块。示意图如下图所示。


image.png

划分成多个标记条带和为并发工作线程设置相应的标记条带需要提前完成,这样才能在标记时把待标记对象直接放入相应的标记条带中,这是第一步开始标记做的工作。

ZGC是根据对象的地址计算对象属于哪个标记条带,把地址通过哈希函数计算得到的值作为标记条带号。

这里其实有一个问题——在对象放入标记条带中存在并发问题。设想这样一种情况,有两个线程T1和T2,有两个标记条带Strip1和Strip2,里面分别存放了Obj1和Obj2。T1标记Strip1里面的对象,T2标记Strip2里面的对象。当T1标记Obj1的时候,需要把Obj1所有的成员变量进行标记,假设发现Obj1的成员变量按照哈希函数计算后需要放入Strip2中,那么T1会访问Strip2,把该对象的成员变量指向的待标记对象入栈。
同理,线程T2也可能访问Strip1。
这和我们前面提到的设计标记条带的目的完全不同。设计标记条带的目的是希望线程T1只访问Strip1,线程T2只访问Strip2,从而完全解决并发性问题。

为了尽可能地让标记线程之间进行并发标记,ZGC对每个标记线程使ZMarkThreadLocalStacks保存需要遍历的对象,当线程的本地标记栈满时,再把标记栈转入ZMarkStripSet中。


image.png

ZMarkStripSet中标记条带是链表的形式,所以放入的时候相当简单,直接插入新的节点到链表中,而无须分配额外的空间。

对于并发工作线程来说,所有的标记条带都没有对象,说明并发工作线程可以结束工作。因为并发工作线程是多线程执行,所以判断的条件是所有工作线程访问的条带都没有对象。对于多线程来说,可能存在有些线程待标记对象少,执行得快,而有些线程待标记对象多,执行得慢的情况,所以并发执行中需要有一种机制来进行负载均衡,让所有并发工作线程尽可能同时结束。并发标记工作线程的负载均衡是通过窃取其他线程的任务完成的,即当本线程没有可以执行的任务时,并不会立即停止线程,而是先从其他的线程窃取任务,然后执行任务。

image.png

因为应用程序线程也可能执行标记,而且应用程序线程标记后,待标记对象存放在应用程序线程的本地标记条带中,所以当并发工作线程结束标记任务后,应该将应用程序线程中的待标记对象转移到并发工作线程的标记条带中,让并发标记线程继续高速工作。因此在并发标记结束过程中设计了主动刷新机制,并发标记0号线程把应用程序线程中的待标记对象刷新到并发线程的标记条带中。继续工作。这就要求应用程序需要能够进入到自己的安全点再去响应主动刷新,对于单个线程走进安全点暂停做别的事情,需要支持HandShake机制。

JDK 10中引入JEP312 Thread-Local HandShake,该项目实现单个线程的暂停,而不是暂停所有线程。HandShake机制也是通过VMThread机制完成的,只不过HandShake中指定了一个暂停的目标线程。

并发转移

image.png

因为是并发转移,所以必然会涉及应用程序线程在转移时访问待转移的对象。此时应用程序线程会先完成转移的任务,然后再访问对象,这就涉及我们之前介绍的读屏障。读屏障的流程图中,会根据页面的状态判断进行转移还是标记或者重定位操作。

思考题2的解答

为什么需要初始转移呢?反正有读屏障不能直接并发转移吗?

应用程序线程正在访问对象,在第10步并发转移中,应用程序线程的访问是需要读屏障的,此时在读屏障中会把对象转移到新的地址。
听起来似乎可行,但是这里有一个问题,我们提到如果应用程序线程正在访问对象,通过读屏障完成转移。这个读屏障只能在第10步中发生,所以它针对的都是在第10步中从根集合中新产生的引用对象,这些新的对象可以通过读屏障得到正确的处理(即新产生的对象被正确地转移),但是可能存在这样一种情况,在进入第10步之前已经通过根集合访问了对象,这时进入第10步后,第10步中的读屏障对于这些已经访问的对象就不起作用了,单从转移角度来说,对象仍然可以被并发转移线程正确地转移。但是从访问角度就会出现问题,此时如果有新的应用程序线程也访问这个对象,如果对象已经被转移,那么这个新应用程序线程通过读屏障访问到新对象,如果对象还未转移,那么这个新应用程序线程则会通过读屏障先转移对象再访问,结论就是两个应用程序线程一个访问老对象,一个访问新对象,如果两个应用程序线程都对对象进行修改,就会发生数据不一致,导致错误。这就是初始转移STW解决的问题。

八, ZGC全流程动画演示

在JVM启动后和垃圾回收发生之前,相关的地址视图会被设置为Remapped。假定应用程序运行一段时间后,整个内存的对象关系如图


image.png

图中对象1 ~ 5位于小页面中,对象6 ~ 11位于中页面中。小页面位于虚拟地址的头部,中页面和大页面位于虚拟地址的尾部,本例中假设不存在大页面对象。小页面占用的空间为2MB,中页面占用的空间为32MB

进入初始标记后,地址视图切换为M0,然后从根集合出发,开始遍历直接引用的对象。
为了更好地突出图中的变化,统一定义虚线表示正在变化的引用关系,标记过程中的活跃对象使用深色背景。初始标记结束后,整个内存的对象关系如图

image.png

对象1、2和4都被标记为活跃的。这里活跃的意思是对象1、2和4的地址视图都变成M0。
同时在初始标记结束后,在标记条带中存在指向对象1、2和4的指针,标记条带将被用于并发标记。假设并发工作线程为两个,对象1、2和4将被放在不同的标记条带中,分别由线程1和线程2并发地标记。


image.png

并发标记时,从标记条带获取对象,开始标记。
注意,在图中初始标记中仅仅把对象1、2和4的地址从Remapped变成了M0,但是并没有记录它们所在页面的活跃对象的信息。
并发标记从对象1、2和4遍历对象的成员变量,同时统计对象1、2和4的信息,这些统计信息放在对象所属的页面中。
这里把这些统计信息称为标记信息,主要包括页面中存活对象的个数,对象经过内存对齐之后占用的内存大小和对象的标记位图信息。

这里没有演示应用程序并发执行的情况,如果应用程序新分配对象,一定是从一个新的页面中分配,因为对象的缓冲页面在初始标记中被清空。

如果是新页面,则不会在本轮垃圾回收中回收,所以在图中没有体现。另外,如果应用程序线程访问待标记的对象,则通过读屏障完成标记,其处理的方法和并发标记中的方法完全一样,图中也没有体现。
在并发标记结束时,所有页面中的活跃对象都被标记,同时标记条带一定为空。进入再标记阶段
再标记主要是为了处理因应用程序线程标记对象,导致仍然有待标记的对象。实际上在并发标记结束前会尝试多次主动刷新(和被动刷新,文章中没有介绍)以避免这种状况,但是这种状况不能完全避免,如果需要完全避免,必须在STW中进行。另外,再标记还会处理部分非强根的标记。为了简化,本例中假定所有的对象在并发标记中都标记完成,也没有非强根引用。

非强引用处理之后,将重置转移集。主要是针对前一次垃圾回收过程中产生的对象地址信息表重置。在本例中因为是第一次垃圾回收所以不会涉及重置。

重置转移集之后,将回收无效的页面。如果在页面分配时内存不足,将回收预分配或者缓存页面中的页面对应的物理地址。本例中假定内存充足,不会执行本步。

在并发标记之后,一共有4个页面被标记。在这一步中,会根据这4个页面的统计信息选择——哪些页面可以回收。ZGC只会选择页面中垃圾空间超过页面空间的25%的页面,然后把所有选择到的页面根据页面中垃圾空间大小排序,根据排序结果计算是否存在一些页面在转移后导致新的页面无剩余空间,如果存在,则把这些页面也丢弃,不进行转移。

在本例中,假定有一个小页面和中页面将被转移,所以它们将进入转移集


image.png

选择转移集结束后,将对转移集中的页面初始化,初始化最终的动作是初始化对象地址转移信息表。转移信息表将存储转移完成后对象转移前后的地址。初始化转移集结束后,整个内存的对象关系如图


image.png

接着将进入初始转移阶段。进入转移阶段后,地址视图再次从M0切换到Remapped。初始转移从根集合出发,遍历对象,对象进行转移或者调整对象的视图。初始转移结束后,整个内存的对象关系如图


image.png

可以看到对象4所在的页面在转移集中,所以它会被转移到新的页面中。对象4转移之后,会在所在的页面中记录对象转移信息。对象1和对象2不在转移集中,所以它们不会被转移,但是它们的地址视图会从M0调整为Remapped。

初始转移只会针对根集合进行,结束后将进入并发转移,并发转移是根据转移集的页面进行遍历,即只会遍历转移集选择的页面。在遍历时,两个并发工作线程会根据标记信息逐个转移对象。转移过程涉及内存的复制,比较耗时,所以在转移时会把一个页面分成64个段并发的转移。因为对象4已经完成转移,所以并发转移会继续转移剩下的对象5和对象8,转移后分别称为对象5’和对象8’。转移后也会更新对象转移信息表。对象5属于小页面对象,转移后也会在小页面中,对象8属于中页面对象,所以转移中会新分配一个中页面来存储对象8。


image.png

需要注意的是,虽然对象4’和对象5’都在一个新的页面中,但是对象4’在并发转移完成后,指向的还是对象5的地址而不会调整到对象5’。

并发转移之后,转移集中的页面会被立即加入页面缓存供新的页面分配使用。
假设转移之后,应用程序线程在执行过程中从对象4’访问了对象5’,从对象8’访问了对象9,此时会利用读屏障对对象进行重定位,此时整个内存的对象关系如图


image.png

对象4’将根据页面中对象地址转移信息表得到对象5的地址为对象5’,对象5’的地址视图是Remapped,所以直接调整引用关系。对象8’访问对象9,对象9并未被转移,它的地址视图仍然为M0,所以此时会先调整对象9的地址视图从M0到Remapped,然后调整引用关系(指向另一个视图为remapped 的9)。

经过一段时间的运行,因为某种原因再次触发垃圾回收。垃圾回收触发,进入初始标记后,地址视图切换为M1,然后从根集合出发,开始遍历直接引用的对象。初始标记结束后,整个内存的对象关系如图


image.png

在新一轮的并发标记中,从标记条带开始标记。在标记时会对页面的标记信息进行复位。这里还要注意,在标记前,对象10、对象11和其他对象稍有区别,其他对象的地址视图为Remapped,对象10和对象11的地址视图为M0,说明它们在上一轮垃圾回收的标记阶段被识别为活跃对象,但是它们所在的页面没有在转移阶段被转移或者被访问。在标记结束后,所有活跃对象的地址视图都会调整到M1


image.png

接下来的步骤和我们介绍过的垃圾回收步骤完全相同,也不再介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • 一个诡异的MySQL查询超时,居然揭开了隐藏了两年的BUG 这一周线上碰到一个诡异的BUG。 线上有个定时任务,这...
    CoderW在简书阅读 501评论 0 1
  • JDK 11中低延迟垃圾回收器,设计目标: 停顿时间 < 10ms,不随堆、活跃对象的大小而增加 支持8M...
    hedgehog1112阅读 949评论 0 1
  • 文章来源于公众号美团技术团队 ,作者王东 王伟 作者简介 王东,美团信息安全资深工程师。 王伟,美团信息安全技术专...
    码农小光阅读 833评论 0 9
  • 一、来源 ZGC收集器是由Oracle公司研发的。2018年创建了JEP 333将ZGC提交给OpenJDK,推动...
    希特文阅读 966评论 1 1
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,393评论 16 21