Java ZGC 工作原理/详解/模型

Java ZGC 工作原理/详解/模型

GC(Garbage Collector) 是JVM(Java Virtual Machine)虚拟机的所提供的重要特性,GC让开发者在使用Java进行应用开发时不再需要考虑进行手工的内存释放(Memory De-allocation),从内存管理相关的工作中释放出来,能够更多地聚焦于程序逻辑本身。

简单来说,GC的核心逻辑在于Java程序可以在运行时动态申请内存,使用完的内存不需要手工释放,而是由GC在适当的时候检测到这些可以被回收的内存并进行释放(对于C/C++语言来说这是令人惊讶的,对于这类语言来说,运行时动态申请的内存必须在使用完后进行手工释放,否则将引起内存泄漏)。Java GC经过多年的发展,已经衍生出多种的GC实现方案,不同的GC实现均包含STW(Stop the world)过程,该过程意味着所有的业务线程均需要停止工作,由GC线程负责进行内存遍历/可回收内存定位/内存压缩等一系列动作,动作结束之后,业务线程恢复工作。STW的可恶之处在于业务线程可能在任何时间点由于GC的STW被暂停,且被暂停的时间长短和应用的堆区(Heap)大小/已创建对象的多少相关。所以GC的实现方向之一就是尽量压缩STW所使用的时间,减少对业务线程的影响。

在Z GC之前,主要的GC实现STW会随着堆区大小的增大而变长,这对于一些大堆Java应用且其实时响应性要求较高的场景,GC变成影响应用性能的关键瓶颈。例如金融业务场景下的实时风控应用,一个交易请求打进去,要求100ms内就要给出对应的风控应答,如果应用本身STW就有100ms,那么无论怎样么优化应用逻辑本身,都无法避免地无法达到应答时效要求。Z GC带来两个可靠的特性:(1)单次GC的STW时间能够降稳定地控制在10ms以内(2)STW时长不会随堆区大小的增长而变长。

了解Z GC算法的实现,直接查看对应的源码实现(25KLOC源码)需要较长的时间去阅读并理清对应的工作模型。 对于开发人员来说,我们希望能够有一个简化的Z GC工作模型作为指导,能够对Z GC的实际工作的上下文有进一步的理解。查看了中文网络上的一些资料,一部分是对Z GC的简单介绍,一部分是直接的源码分析,缺少一个中间层,介绍Z GC工作模型的介绍,本文基于ACM论文<Deep Dive into ZGC: A Modern Garbage Collector in OpenJDK>,介绍Z GC的具体工作模型。文章约8500字,常规阅读时间约40分钟。

基础知识

在了解Z GC实现之前,需要一些先决的基础知识,这一部分网络上的资料较为丰富,因此这里只是简述这一部分基础。

堆内存对象可达性分析

在程序运行过程中,GC算法会在某一个时间点被触发,从而开启对整个堆区内存的分析,分析的结果是留下还在被业务线程使用的对象(可达对象),清理掉不再使用的对象(不可达对象)。对于这一过程,有很多的实现算法,比如引用计数法(Reference Counting)、GC根遍历法(GC Root Traversing)。GC根遍历法是多数GC算法使用的方式,所谓的GC根主要是全局的类对象指针,例如方法区上类的静态对象指针,业务线程栈帧上的对象指针。以这些对象指针作为起始点,开始遍历找到所有的可达对象并进行标记,那么剩下的未被标记的对象就是不可达对象,可以被清理掉的对象。

对象分配方式

到现在为止,Java在堆区分配对象的方式分为两种:一种是分代式对象分配,一种是页式对象分配。

对于分代式模型,主要的理论依据是不同对象的存活生命周期不同,80%的对象生命周期较短,20%的对象才是生命周期较长。分代式模型将堆内存划为两个不同的区域,Young区(生命周期较短的对象存储的区域)和Old区(生命周期较长的对象存储的区域)。对于new一个对象的请求,先将对象放到Young区,Young区的对象如果存活时间达到一定的阈值,那么将被迁移到Old区。对于Young区和Old区的对象,可以使用不同的回收算法进行不可达对象的分析和处理。

对于页式模型,将堆内存划分为大小相同的页,对于new一个对象的请求,将对象分配到一些指定的页上,GC过程中会将这些指定的页上的存活对象迁移到其他的页上,以便可以快速的回收这些页。

对象回收方式

对象回收过程需要面对的一个问题是回收后内存的整洁程度问题。如果只是清理不可达对象所对应的内存区域,那么经过多次的GC迭代,整个内存区域会变得非常零散,你不可能有一个平整的可用内存空间,进而导致有足够的内存却无法创建一个新的对象。对于分代式对象分配模型,对象回收过程在Young区使用复制模型(copy mode)将所有的可达对象集中复制到内存区域的一端。在Old区,类似Young区的复制模型,但是由于Old区较大,且内存区域不像Young区分分两段使用,复制的过程更加复杂。对于页式对象分配模型,会将指定页上的可达对象复制到其他页上,进而可以直接回收这些完全空白的页。

Z GC工作模型

模型概述

对于Z GC,一次完整的GC周期由多个阶段(Cycle)组成:

  • STW-1: 开启一次全新的GC周期,(1)设定STW-1开始后到STW-3开始前的全局有效颜色(M0或M1),全局有效颜色决定此后新创建的对象指针的颜色,同时对GC线程的MB(Mark Barrier)过程以及业务线程的LB(Load Barrier)过程在指针有效性判定上至关重要(2)分配新的内存页(Z GC使用页式对象分配模型)用于GC开启后容纳新创建的对象,本次GC周期只针对STW-1之前已经分配的页进行GC,不对STW-1开启之后创建的对象进行GC(3)遍历GC根指针,使用MB过程对根指针进行处理,创建根遍历栈(root marking stack),将处理过的根指针保存到遍历栈当中;
  • Marking/Remapping(标记/指针重定向): 基于根遍历栈,使用GC根遍历法,GC线程开启全局的堆内存对象可达性分析,标记所有的可达对象。并且染色遍历过程中所遍历到的指针的指针颜色为全局有效颜色。在Marking/Remapping阶段,业务线程同时运行,Z GC的特别之处在于增加了LB过程,对于业务线程需要加载到线程栈使用的对象指针,使用LB过程判断对象指针的指针颜色有效性。如果指针颜色不同于全局有效颜色,则判定指针为无效指针。对于无效的指针进行指针地址修复,即指针重定向(Remapping);
  • STW-2: GC线程判断全局的堆内存对象可达性分析是否结束,即是否已经遍历完所有的对象。如果可达性分析已经结束,那么将触发Selection of evacuation candidates;
  • Selection of evacuation candidates1(待回收页确定): 计算所有的STW-1之前分配的页中的存活对象数量,对于存活对象较少的页,将其判定为待回收页(evacuation candidates);
  • STW-3: 为Relocation做必要的准备,包括(1)确定STW-3到下一个GC周期STW-1开始前的全局有效颜色为R(2)遍历GC Root中的指针,如果指针指向上一步确认的待回收页中的对象,则迁移对象到新的页面中,对相应的GC指针进行指针地址修复;
  • Relocation(待回收页对象迁移): GC线程将待回收页中的对象迁移到新的页面中。Relocation阶段业务线程同时运行,业务线程在访问对象期间,基于指针颜色判断并辅助将待回收页中的对象迁移到新的页面中;

Z GC相较于之前的GC算法,其STW可以稳定压缩的核心理念在于引入了染色指针和LB过程。染色指针是实施LB过程的先决条件,染色指针的核心在于其携带了指针所指向的对象的创建时机。LB基于这样的对象创建时机信息,判断是否需要对对象做特殊处理。基于这样的方式,可以实现两个目标(1)STW过程的时间长短只与GC Root中对象的数量有关,而与整个堆上对象的数量无关(2)业务线程不需要等待GC线程将所有的对象迁移到新的地址后才能运行,而是在运行过程中使用LB过程判断并发现悬空指针,而且可以基于染色指针只对少量的指针进行悬空指针判断。

染色指针

在64位操作系统上,Java应用的指针(引用)由8 byte/64 bit构成。对于Z GC,64 bit的指针由三部分组成,其中低42位为应用的实际地址空间,中间4位为指针颜色标识位(当前支持4种颜色,其中R/M1/M0为实际使用的颜色),高18位未使用。

三种颜色中M0/M1两种在连续的GC周期的STW-1阶段循环使用,如果Cycle N的SWT-1阶段使用颜色M0,那么Cycle N+1的STW-1阶段使用颜色M1,Cycle N+2的STW-1阶段使用颜色M0,Cycle N+3的STW-1阶段使用颜色M1,如果循环往复。所有的GC周期的STW-3阶段均使用颜色R。

染色指针的使用可以分为两个维度:

  • 在指针生成维度,每当我们创建一个新的对象时,我们一般会创建一个指向到该对象的指针。在应用程序运行的每个时刻,都有一个该时刻的全局有效颜色定义,创建的指针的颜色标识位使用该全局有效颜色定义的颜色;
  • 在指针访问维度,业务线程在加载一个堆对象指针时使用LB过程进行指针处理。LB过程会先基于指针的颜色判断指针是否有效,判断也是基于该时刻的全局有效颜色定义,如果指针的颜色与全局有效指针颜色定义相同,则认为指针有效,否则认为指针无效。对于无效的指针,需要进一步确定是否需要进行Remapping或者Relocaton操作;

以Cycle N的SWT-1阶段为例,假设该阶段的全局颜色定义为M0。那么在其后的Marking/Remapping阶段,业务线程在加载指针时可能遇到三种颜色的指针。如果指针颜色为M0,那么可以判断该指针要么为STW-1阶段后新创建的对象的指针,要么为已经经历过指针颜色修复的指针(即该指针所指向的对象为上一GC周期创建的对象,LB过程在之前对该指针的访问过程中已经修复其颜色为当前有效颜色M0)。如果指针颜色为M1,那么该指针所指向的对象是上一GC周期的STW-1阶段到STW-3阶段之间创建的对象。如果指针颜色为R,那么该指针所指向的对象是上一GC周期的STW-3阶段到本周期的STW-1阶段之间创建的对象。Marking/Remapping阶段LB过程会对上一周期产生的颜色为M1/R的指针进行悬空指针修复(由于上一GC周的Relocation阶段会产生悬空指针)。

以Cycle N的STW-3阶段为例,该阶段的全局颜色定义为R。那么在其后的Relocation阶段,业务线程在加载指针时可能遇到两种颜色的指针。如果指针颜色为R,那么可以判断指针要么为STW-3阶段后新创建的对象的指针,要么为已经经历过指针颜色修复的指针。如果指针颜色为M0,那么该指针所指向的对象为本GC周期的STW-1阶段到STW-3阶段之间创建的对象或者是本GC周期的Marking/Remapping阶段所处理过的对象。Relocation阶段LB过程会对本周期产生的对象迁移任务进行辅助迁移和悬空指针修复。

STW-1

进入STW-1阶段表明Z GC预期应用的堆内存即将不足,并开启新一轮的堆内存对象回收以保证本轮GC结束时业务线程有足够的堆内存可以使用。应当知道的前提是(1)堆内存中对象指针包含两种可能的颜色,R+M0(M0对应于上一次GC Cycle的STW-1阶段定义的全局有效颜色)(2)堆内存中对象指针中可能存在悬空指针(悬空指针的定义见词汇表,概括讲是指上一个GC Cycle的Relocation阶段将部分对象的内存位置迁移了,指向这些对象的指针并没有随之修改导致这些指针变为悬空指针)。

Mark barrier过程被GC线程所使用,GC线程使用该过程判断GC Root指针是否为悬空指针,对于悬空指针使用remap操作将其修复,其中slot参数为含有GC Root指针的内存地址,addr为对应的GC Root指针值。Mark barrier判断如果addr为以上一个GC周期中指向Evacuation Candidate页列表中的对象,那么可以判断为悬空指针,remap操作使用地址映射表基于老的指针值获得新的有效指针值。如果不是这种情况,那么直接修改指针颜色为全局有效颜色。并且将GC Root指针加入mark stack。self_heal操作基于CAS操作将slot内存地址的指针只修改为有效指针值。

STW-1阶段完成三件事情:

一是定义新的全局颜色。如果上一次GC Cycle的全局颜色为M1,则本次定义为M0。如果上一次GC Cycle的全局颜色为M1,则本次定义为M0. 这样做的结果是堆内存上的所有指针均变为无效的指针(因为当前堆内存上的指针所有可能的颜色为M0+R或者M1+R),从而导致后续的M/R阶段对所有的堆内存指针,均执行一次且仅执行一次悬空指针判断并对悬空指针执行Remapping操作。

二是申请新的内存页用于存放STW-1结束后业务线程申请的新对象。需要注意的是本轮GC Cycle主要对STW-1阶段之前存在的对象进行回收,而不会对STW-1结束后到下一次GC Cycle STW-1开始前分配的对象进行回收。

三是对于所有的GC Root引用,执行如上所述的Mark barrier过程,(1) 将所有的GC Root指针的指针颜色修改为当前全局颜色定义,(2) 判断所有的GC Root指针是否为悬空指针,如果时则执行remap操作获取新的有效指针地址,(3) 同时将GC Root引用放到root marking stack中,作为起始点,用于后续的堆内存对象遍历+标记。

M/R(Marking/Remapping)

M/R阶段GC线程使用GC根遍历法遍历整个堆区。业务线程则正常执行业务代码但在指针加载过程中需要使用LB过程进行指针处理。其中:

GC线程基于root marking stack,开始对堆内存进行对象遍历+标记,对于每一个访问到的对象指针,均需要执行Mark barrier过程,对于所有遍历到的指针,(1)修改指针颜色修改为当前全局颜色定义,(2) 判断所指针是否为悬空指针并进行remap操作 (3) 将该指针放到marking stack中继续进行对象遍历。

业务线程继续执行业务逻辑,对于执行逻辑中需要从堆内存中加载对象指针的操作,在该操作前均需插入Load Barrier过程(如下图所示),如果发现该指针的颜色不同于当前全局颜色定义(发生该场景的原因在于该指针还没有被GC线程的堆内存遍历过程遍历到,所以没有进行指针颜色修改和悬空指针判定),那么业务线程辅助进行(1) 将该指针的指针颜色修改为当前全局颜色定义,(2) 判断该指针是否为悬空指针,如果时则执行remap操作将指针地址修复为有效的地址,(3) 将该指针放到marking stack中用于辅助GC线程进行对象遍历。

M/R阶段的完成达到了四个效果:

  • 堆内存中的所有指针,指针的颜色都修改为全局颜色定义所指定的颜色;
  • 堆内存中的所有指针,都进行了悬空指针判断并对于发现的悬空指针执行了remapping操作;
  • 堆内存中的所有对象,经过对象可达性分析,所有的可达对象都打上了标记,未标记的对象均未不可达对象;
  • 所有已分配的内存页(不包含STW-1阶段分配的内存页)中的可达对象数量都已经被统计,后续基于每个内存页中的剩余可达对象的数量,判断是否需要回收该内存页(回收的规则简单说就是如果一个内存页中剩余的可达对象数量越少,那么越可能回收该内存页。回收的过程是将该内存页中剩余的可达对象复制到其他的内存页中,从而可以完全回收该内存页);

STW-2

STW-2阶段主要是判断M/R阶段marking stack中是否还有对象遗留。如果marking stack已经为空,那么说明GC线程在M/R阶段已经完成堆内存对象遍历,所有可达对象均已被明确标记出俩,可以开启下一个EC阶段。因此该阶段的主要工作就是判断并结束M/R阶段。

EC(Selection of Evacuation Candidates)

STW-2的结束意味着(1) M/R阶段的堆内存对象遍历已经完成 (2) 上一个GC周期所产生的所有悬空指针已经得到修复。在该阶段,业务线程将继续其对应的工作(由于当前堆内存中的所有指针的颜色都修改为同全局颜色定义,所有业务线程在加载堆内存指针执行Load Barrier过程时不会再一次进入悬空指针判断的逻辑)。而GC线程则开始对上一GC周期所使用的内存页进行排序,排序基于内存页中剩余的可达对象数量,如果剩余的可达对象数量越少,其排名越高。基于一个指定的阈值,将可达对象数量小于该阈值的内存页列入Evacuation candidates,这些内存页中的对象后续将在Relocation阶段将复制到其他内存页当中,这些内存页将被完全回收。

总结下来,EC阶段GC线程完成如下的操作:

  • 前一个GC周期所建立的对象地址映射表(Storing Forward Table) 所使用的内存空间被释放(因为所有的悬空指针在M/R阶段已经全部被修复掉了,所以映射表已经没有存在的必要了);
  • 基于排序统计,获得了所有的需要在本GC周期进行回收的页面,即放到Evacuation candidates中的页面;

STW-3

本次GC周期的最后一个STW阶段,主要是为后续的RE阶段进行准备工作,准备包含三方面的工作:

一是将全局颜色定义更新为R。这将导致本GC周期内所有的堆内存中的指针均变为失效的指针(因为本GC周期的STW-1阶段到STW-3阶段,所有的指针的颜色,包括已经存在的和新建的指针,均被修订为STW-1所定义的全局颜色);

二是准备新的对象地址映射表(Storing Forward Table)。这个新的对象地址映射表将在RE阶段所使用,用于存放被迁移对象的新旧地址之间的映射;

三是对于所有的GC Root指针,进行遍历并对每一个指针进行操作(1)将指针的颜色修改为R,以保持和全局颜色定义的统一(2)判断指针所指的对象是否为在EC阶段所创建的Evacuation candidates中,如果是那么将对象迁移到新的内存页中,并且将GC Root指针值修改为新的对象地址。这样做的目的是保持所有的GC Root指针均指向有效的对象。进一步讲,由于Evacuation candidates中的内存页均是需要回收的内存页,而GC Root指针的并不会被后续的RE阶段所修正,所有需要再STW-3阶段进行修正。

RE(Relocation)

RE阶段GC线程对EC阶段所认定的EC页中的对象进行对象迁移。业务线程则正常执行业务代码但在指针加载过程中需要使用新的LB过程进行指针处理。其中:

GC线程会将EC内存页中的对象迁移到其他的内存页中,并将对象的新旧地址之间的映射放到对象地址映射表(Storing Forward Table)中。其中左边的函数依次遍历每个Evacuation candidates中的内存页,将内存页中的对象重定向到新的位置(注意这一过程并不会修改对应的指向这些对象的指针,所有指向这些对象原地址的指针立刻成为悬空指针),然后将整个内存页释放掉。右边的函数使用CAS的方式将对象插入到对象地址映射表中。

业务线程继续执行业务逻辑,但是在加载堆内存中的指针时需要执行如下的新的Load Barrier过程,基于指针颜色判断指针有效性并辅助GC线程进行对象的迁移(这里有两种可能,如果GC线程已经完成对象的迁移,那么Load Barrier将直接跳过迁移过程,如果GC线程在业务线程访问对象时还没有完成迁移,那么将由业务线程进行对象的迁移)。

RE阶段没有明确结束的标志,实际上是以GC线程完成所有的对象迁移作为逻辑上的结束标志。在RE阶段结束后,由于存在对象迁移的行为,那么堆内存上会存在指向这些对象的指针,它们已经成为悬空指针。如果业务线程访问到悬空指针,如上图的LB函数会修复该悬空指针。但是没有被业务线程访问到的悬空指针则需要等到下一个GC周期的Marking/Remapping阶段才能被最终修复掉。不过这并不影响业务线程的正常执行,因为LB会保证所有被引用到的指针都是有效的。

Z GC过程图示

词汇表

  • GC线程 – 专门用于GC处理的线程;
  • MB(Mark Barrier) – GC线程使用根遍历法的过程中使用MB进行对象指针进行判定处理。处理包括两个方面:(1)判断并修复指针的指针颜色,使得指针颜色与当前全局指针颜色保持一致(2)判定并修复悬空指针;
  • LB(Load Barrier) – 业务线程在加载对象指针时使用,对于被加载的指针进行判断处理。处理包括两个方面:(1)判断并修复指针的指针颜色,使得指针颜色与当前全局指针颜色保持一致(2)判定并修复悬空指针;
  • 业务(mutator)线程 – 由应用程序创建并从应用业务处理的线程;
  • 染色指针/染色引用(Colored Pointer) – 对于普通的指针/引用,其内容包含所指向对象的对象地址。而染色指针/染色引用除包含对象地址外,使用额外的位(bit)包含对象的Color信息。对于Z GC实现,存在三种可能的颜色(Color),Color M0/Color M1/Color R,Color的意义在于标识指针或者引用是否处于一个指定的可用状态,应用辅助GC线程/业务线程判断是否需要对指针/引用进行特殊的处理;
  • 指针修复 – GC线程或者业务线程在访问染色指针时发现指针的颜色不正确,进而发起对指针的检查并修复可能存在的指针指向不正确的问题,同时将指针的颜色修复为正确的颜色;
  • 悬空指针(Dangling Pointer) – 由于Relocation阶段将存活对象从evacuation page中迁移到其他的fresh page的过程中,并不会修复引用这些存活对象的指针,所有的这样的指针为悬空指针,将在后续应用线程访问指针或者在Marking/Remapping阶段进行指针修复;
  • Remapping操作 – 指针修复的一种,判断指针所指向的内存地址是否指向Evacuation Candidates内存页(如果是那么对象已经迁移从这些内存页迁移到其他的fresh内存页),如果是那么需要基于Storing Forward Table,将指针的内容修复为有效的迁移后的内存地址;
  • 对象地址映射表(Storing Forward Table) – 对于Evacuation Candidates页中的对象,在对象迁移到fresh页之后,该表存储对象在Evacuation Candidates页中的地址到fresh页中的地址之间的映射;
  • Relocate操作 – 将Evacuation candidates中内存页上的对象迁移到fresh内存页的上的操作;

Z GC触发时机

除Z GC和G1之外的其他GC算法,都是在内存空间不足以分配新的对象时,触发对应的GC算法以回收堆区内存空间。而Z GC算法是基于一个启发式推断的时间,在Z GC算法认为内存在未来的某一时间点A将不足以分配支持业务线程创建新的对象,且当前时间点执行GC算法可以确保在时间点A执行完以保证业务线程继续有足够的内存分配新的对象时,触发执行新的Z GC周期,其目标是保证业务线程在任何时间点都能够创建新的对象。当然算法的这样的推断并不能够100%保证成功(比如业务线程的TPS突然达到峰值,那么短时间会申请大量的内存创建对象),所以如果业务线程不能够创建新的对象时,那么将触发一次全局性长延时GC,这是在使用Z GC时需要考虑的一点。

吞吐量/响应性取舍

Z GC在业务线程访问对象指针的时候,增加了一个Load Barrier屏障函数用于确保能够访问到正确对象指针。因此增加了相应的开销,这些开销实际上是将影响应用的吞吐量。所以各种GC实现本质上是在吞吐量和响应性之间的取舍,使用Z GC带来了更好的响应性,但是会降低对应的应用吞吐量。使用其他的GC算法,可能应用的响应性不佳,但是会带来更好的吞吐量。这就要看应用本身的目标是趋向于响应性还是趋向于吞吐量。对于批处理类的应用,由于趋向于吞吐量,所有使用Z GC并不是最佳的选择。而对于API接口服务类应用,那么Z GC可能是更佳的选择。

参考

  1. Reference Processing并不在本次模型的分析中,其主要是用于对非强引用类型进行的特殊处理。如果对这一部分感兴趣,可以直接阅读对应论文的第5章节。 ↩︎

One thought on “Java ZGC 工作原理/详解/模型

Leave a Reply

Your email address will not be published. Required fields are marked *