大牛用十年功力带你彻底理解JVM垃圾回收器:ZGC,回收设计

2023-10-27

回收设计

ZGC的并发回收算法采用的也是“目的空间不变性”的设计,关于目的空间不变性的更多内容可以参考第7章。

在第7章中提到,Shenandoah从JDK 13开始也采用“目的空间不变性”的设计。但是ZGC与Shenandoah相比,还是有不少细节并不相同,如表8-3所示。

表8-3 Shenandoah和ZGC比较

添加图片注释,不超过 140 字(可选)

本节主要围绕ZGC算法的特殊点进行介绍。

算法概述

ZGC基于分区管理,在回收的时候采用的是单代、部分回收,即选择部分垃圾比较多的分区进行回收。整个回收算法经历3个阶段,分别是标记、转移和重定位。

在使用ZGC时,堆空间按照页使用,在启动垃圾回收时,部分页面已经满了,其中有活跃对象也有死亡对象。另外,整个堆空间还有一部分空闲的页面,这些页面用于垃圾回收过程中对象的转移。关于垃圾回收启动的时机,后文将详细介绍。

在JDK 16之前,ZGC进行回收时一定需要空闲的页面才能完成对象的转移,这实际上降低了内存的利用率。在JDK 16中引入了新的优化,在一定条件下对象的转移还是在本页面内(称为in-place relocation)进行,也称为本地转移。

在标记阶段完成堆内活跃对象的识别。ZGC的标记阶段分为初始标记、并发标记、再标记和弱根标记。

1)初始标记:使用STW的方式,暂停Mutator,完成从根集合出发到堆内对象的标记。

2)并发标记:将根集合识别出的活跃对象作为并发标记的起点,完成整个堆空间内活跃对象的标记。

3)再标记:使用STW的方式,暂停Mutator,再次完成根集合到整个堆内空间的标记。再标记主要是为了解决某些Mutator在并发标记阶段因各种因素,无法执行新增根集合到堆空间的标记的问题。

4)弱根标记:此阶段处理弱根(包括Java语言中的引用),弱根处理的目的是将标记阶段识别出来的对象再次进行标记处理,确定这些对象是否真的活跃。

关于标记的实现将在8.3.1节介绍。在标记完成后,整个堆空间的状态如图8-5所示。

添加图片注释,不超过 140 字(可选)

图8-5 标记完成后堆空间状态

在标记完成后,进入转移阶段。在进入转移阶段时,会选择到底转移哪些页面(活跃对象比较多的页面不转移,以提高转移的效率)。ZGC的转移阶段分为初始转移和并发转移。

1)初始转移:使用STW的方式,暂停Mutator,完成从根集合出发到堆内转移集合对象(即对象必须位于转移页面)的转移;根集合中引用到的不在转移集合中的对象则不会转移。

2)并发转移:根据选择的转移集合,对其中的活跃对象进行转移。

关于转移的实现在8.3.2节介绍。转移完成后,整个堆空间的状态如图8-6所示。

添加图片注释,不超过 140 字(可选)

图8-6 转移完成后堆空间状态

在转移的过程中使用了转移表(Forwarding Table)来记录对象转移前后的地址。这样在转移完成后,转移集合中的页面都可以被释放,然后被立即重用。 当转移完成后释放转移完成的分区,整个堆空间的状态如图8-7所示。

添加图片注释,不超过 140 字(可选)

图8-7 释放转移完成的分区后堆空间的状态

在转移完成后,进入重定位阶段。在ZGC的实现中将重定位和标记阶段进行了合并。在标记的时候,如果发现对象使用了过时的对象(例如这个对象发生了转移),只需要从转移表中根据当前的地址找到转移后的地址,并更新相关引用地址即可。

在ZGC的设计中采用了目标空间不变性来保证并发操作的正确性。在实现中通过读屏障来完成,当读到堆内对象时,首先判断对象状态是否正确,如果不正确,则通过屏障来保证对象的正确性。所以在标记阶段,读屏障的目的是帮助标记活跃对象;在转移阶段,读屏障的目的是帮助将转移集中的活跃对象转移到新的页面中。

为了区别不同的阶段,ZGC引入了视图状态,并在不同的阶段采用不同的读屏障。下面介绍一下视图和读屏障。

视图状态

ZGC使用了3种视图状态,分别为Marked0(也称为M0)、Marked1(也称为M1)和Remapped。其中M0和M1表示标记阶段,Remapped表示转移阶段。ZGC在初始化之后,整个内存空间的地址视图被设置为Remapped,当进入标记阶段时,视图转变为M0或者M1,标记阶段结束进入转移阶段时,视图再次被设置为Remapped。采用视图表示后,具体的算法如下。

(1)初始化阶段

在ZGC初始化之后,地址视图为Remapped,程序正常运行,在内存中分配对象,满足一定条件(关于垃圾回收的触发时机将在后文介绍)后垃圾回收启动。此时进入并发标记阶段。

(2)并发标记阶段

第一次进入并发标记阶段时视图为M0,在并发标记阶段应用程序和标记线程并发执行。那么对象的访问可能来自:

1)GC工作线程。GC工作线程访问对象的目的就是对对象进行标记。它从根集合开始标记对象,在标记前先判断对象的地址视图是M0还是Remapped。

如果对象的地址视图是M0,则说明对象是在进入并发标记阶段之后新分配的对象,或者对象已经完成了标记,也就是说对象是活跃的,无须处理。

如果对象的地址视图是Remapped,则说明对象是前一阶段分配的,而且通过根集合可达,所以把对象的地址视图从Remapped调整为M0。

2)Mutator运行用户代码时访问对象,所做的工作有:

如果Mutator创建新的对象,则对象的地址视图为M0。

如果Mutator访问对象并且对象的地址视图是Remapped,则说明对象是前一阶段分配的,只要把该对象的视图从Remapped调整为M0就能防止对象漏标。注意,只标记Mutator访问到的对象还不够,实际上还需要标记对象的成员变量所引用的对象,可以通过递归的方式完成标记(为了不影响Mutator的运行,该工作将会转入GC工作线程中完成)。

如果Mutator访问对象并且对象的地址视图是M0,则说明对象是在进入并发标记阶段之后新分配的对象或者对象已经完成了标记,无须额外处理,直接访问。

总之,在标记阶段结束之后,对象的地址视图要么是M0,要么是Remapped。如果对象的地址视图是M0,则说明对象是在标记阶段被标记的或者是新创建的,是活跃的;如果对象的地址视图是Remapped,则说明对象在标记阶段既不能通过根集合访问到,也没有Mutator访问它,所以是不活跃的,即对象所使用的内存可以被回收。

当并发标记阶段结束后,ZGC使用对象活跃信息表记录所有活跃对象的地址,活跃对象的地址视图都是M0。

(3)并发转移阶段

标记结束后就进入并发转移阶段,此时地址视图再次被设置为Remapped。转移阶段会把部分活跃对象(只有垃圾比较多的页面才会被回收)转移到新的内存中,并回收对象转移前的内存空间。在并发转移阶段,应用程序和标记线程并发执行,那么对象的访问可能来自:

1)GC工作线程。GC工作线程根据标记阶段标记的活跃对象进行转移,所以只需要针对对象活跃信息表中记录的对象进行转移。当转移线程访问对象时:

如果对象在对象活跃信息表中并且对象的地址视图为M0,则转移对象,转移以后对象的地址视图从M0调整为Remapped。

如果对象在对象活跃信息表中并且对象的地址视图为Remapped,则说明对象已经被转移,无须处理。

2)Mutator运行用户代码时访问对象,所做的工作有:

如果Mutator创建新的对象,则对象的地址视图为Remapped。

如果Mutator访问对象并且对象不在对象活跃信息表中,则说明对象是新创建的或者对象无须转移,无须额外处理。

如果Mutator访问对象并且对象在对象活跃信息表中,且对象的地址视图为Remapped,则说明对象已经被转移,无须额外处理。

如果Mutator访问对象并且对象在对象活跃信息表中,且对象的地址视图为M0,则说明对象是标记阶段标记的活跃对象,所以需要转移对象。在对象转移以后,对象的地址视图从M0调整为Remapped。

至此,ZGC一个垃圾回收周期中并发标记和并发转移就结束了。我们提到,在标记阶段存在两个地址视图M0和M1。上面的算法过程显示只用到了一个地址视图,为什么设计成两个呢?简单地说,是为了区别前一次标记和当前标记。

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

那么下一次垃圾回收标记阶段开始的时候,存在两种地址视图的对象,对象的地址视图为Remapped说明在并发转移阶段被转移或者访问过;对象的地址视图为M0,说明在前一次垃圾回收的标记阶段被标记过。如果本次垃圾回收标记阶段仍然使用M0这个地址视图,那么就不能区分对象是否是活跃的,还是上一次垃圾回收标记过的。所以新一次标记阶段使用了另外一个地址视图M1,则标记结束后所有活跃对象的地址视图都为M1。此时这3个地址视图代表的含义如下。

1)M1:本次垃圾回收中识别的活跃对象。

2)M0:前一次垃圾回收的标记阶段被标记过的活跃对象,对象在转移阶段未被转移,但是在本次垃圾回收中被识别为不活跃对象。

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

这里通过一个简单的场景来演示并发标记算法。假设在ZGC初始化后,Mutator创建对象0、对象1和对象2,此时它们的地址视图都是Remapped。之后因为某种因素触发垃圾回收,则进入标记阶段。在标记阶段假设对象0和对象2可以通过根集合访问并标记,另外,应用程序线程在并发运行过程中新创建对象3。标记结束后发现对象1不可以从根集合访问到,此时对象0、对象2和对象3的地址视图为M0,表示为活跃对象,对象1的地址视图还是Remapped,表示为垃圾对象,如图8-8所示。

添加图片注释,不超过 140 字(可选)

图8-8 并发算法初始和标记阶段示意图

标记阶段结束之后,进入转移阶段。假设应用程序线程并发运行过程中新创建对象4;对象0所在的页面需要回收,所以对象0转移到新的页面,这个新的对象称为对象0’;对象2和对象3没有被访问到。此时对象2和对象3的地址视图为M0,对象4和对象0’的地址视图为Remapped,对象0所在的页面将被回收,对象1所在的页面可能被回收,也可能不被回收。如果对象1所在的页面被回收,则对象1不存在,如果页面没有回收,则对象1的地址视图为Remapped,如图8-9所示。

添加图片注释,不超过 140 字(可选)

图8-9 并发算法转移阶段示意图

经过一段时间的运行后,再次触发垃圾回收。因为对象1不活跃,下一次垃圾回收时也不会被标记,所以我们不再关注对象1。假设在新的标记阶段,只有对象4从根集合可达,对象2、对象3和对象0’都是不可达的,对象6是应用程序新分配的对象。此时对象2和对象3的地址视图为M0,表示垃圾对象,对象0’的地址视图为Remapped,也表示为垃圾对象,对象4和对象6的地址视图为M1,表示活跃对象,如图8-10所示。

添加图片注释,不超过 140 字(可选)

图8-10 并发算法新一轮垃圾回收的标记阶段示意图

标记完成后会再次进入转移阶段,转移阶段和前一次转移阶段过程类似,不赘述。

读屏障

由于ZGC是并发执行,也就是说Mutator和GC工作线程可以同时修改同一个对象,如果没有合理的同步机制,将导致运行出错。Mutator修改对象是为了程序的正常执行,而GC工作线程修改对象是为了垃圾回收。两者虽然可能会同时修改同一个对象,但它们所做的事情完全不同。

一种常见的保证正确性的设计是:Mutator在修改对象前先做GC工作线程的工作,然后再修改对象,这样GC工作线程就不用与Mutator竞争修改对象了。

ZGC采用读屏障的方式来确保正确性。即Mutator在读对象的时候,判断GC周期正在执行的操作,然后判断访问的对象是否已经执行了GC的操作,如果没有执行,那么Mutator先执行GC操作,再继续访问对象。

读屏障的具体实现是在字节码层面或者编译代码层面给读操作增加一段额外的处理(执行对应的GC操作)即可。

读屏障是由读命令触发的。JVM有3种运行状态:解释执行、C1和C2优化执行。不同的运行状态,读屏障的触发代码略有不同,但它们使用的读屏障是完全一样的。我们从最简单的解释执行看一下读屏障的实现。读屏障在解释执行时通过load相关的字节码指令加载数据。大家可以参考相关的书籍或者文章了解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就是读屏障,在对象加载完成后做额外的处理。这里不分析具体的代码,直接给出ZGC中读屏障的流程图,如图8-11所示。

添加图片注释,不超过 140 字(可选)

图8-11 强引用使用的读屏障

添加图片注释,不超过 140 字(可选)

图8-12 非强引用使用的读屏障

整个读屏障会根据垃圾回收的阶段来判断执行什么操作,操作有标记、转移和重定位。

1)标记:将对象标记为活跃对象,在Mutator进行标记后,还需要标记已标记完成对象的成员变量,但为了减少标记对于Mutator的影响,一般将对象送入GC工作线程中标记。为了减少Mutator和GC工作线程之间的影响,需要设计无锁的数据结构来处理这种情况。ZGC使用线程局部栈的结构保存每个Mutator需要遍历的对象,在Mutator的本地标记栈满的情况下,会将其放入GC工作线程的待标记数据结构中。

2)转移:将转移集中的对象转移到新的页面中。Mutator辅助转移仅仅转移对象本身,不会做额外的事情,GC工作线程负责页面集中其他所有对象的转移。但是Mutator的转移有潜在的两个问题。

当有大量的对象需要Mutator辅助转移时,Mutator的效率会下降;当Mutator在转移时遇到没有可供待转移对象分配的内存空间时,会导致Mutator本身暂停。

为了保证转移的效率,Mutator辅助转移和GC工作线程的转移通常使用不同的目标内存,减少锁的使用。这在一定程度上破坏了内存数据的局部性。

3)重定位:发现对象的地址过时(发现对象在上一次GC周期已经转移)时,应根据转移表获取对象转移后的地址,并更新该值即可。

高效的标记和转移设计

在GC的实现中,两个关键的操作分别是标记和转移。在其他的垃圾回收实现中标记需要修改对象头,设置一个特殊的状态表示对象是活跃的。而设置对象头需要发生一次真实的内存访问,并将对象头修改写回内存。

ZGC中采用了一种称为Color Pointer的机制来避免这样的内存访问。具体的思路是:借助于对象的地址位,在地址位上设置不同的标记状态。例如使用一个地址位表示对象是否活跃,当设置为0时表示对象死亡,设置为1时表示对象活跃。那么标记时不再需要修改对象头,只需要修改对象的地址位即可。这样做的好处就是标记对象存活根本不需要真正访问对象,从而减少了因为GC工作频繁地访问内存。

在上面介绍了ZGC使用视图状态来描述GC的工作状态。把视图状态和Color Pointer结合,即用地址位来描述视图状态,既可以表达GC的工作状态,又可以减少内存的访问。

在JDK 11和JDK 12中,ZGC支持的最大的堆空间为4TB。从JDK 13开始,支持的最大的堆空间为16TB。其中最主要的原因就是ZGC使用了对象不同的地址位。我们先以JDK 11为例来介绍一下ZGC如何使用地址位。

ZGC支持64位系统,以ZGC支持4TB堆空间为例,看一下ZGC是如何使用64位地址的。ZGC中低42位(第0~41位)用于描述真正的虚拟地址,接着的4位(第42~45位)用于描述元数据,其实就是上面所说的Color Pointer,还有1位(第46位)暂时没有使用(所以也设置为0),最高17位(第47~63位)固定为0,如图8-13所示。

添加图片注释,不超过 140 字(可选)

图8-13 ZGC支持4TB空间地址位使用示意图

42位地址最大的寻址空间就是4TB。在JDK 13中堆空间扩展为16TB,其地址位使用的示意图如图8-14所示。

添加图片注释,不超过 140 字(可选)

图8-14 ZGC支持16TB内存地址位使用示意图

从JDK 13开始,ZGC设计为支持4TB、8TB和16TB的内存。那么是否可以支持更大的内存空间呢?目前来说非常困难,主要是受硬件限制。目前大多数处理器地址线只有48条,也就是说64位系统支持的地址空间为256TB。为什么处理器的指令集是64位的,但是硬件仅支持48位的地址呢?最主要的原因是成本。即便到目前为止由48位地址访问的256TB的内存空间也是非常巨大的,也没有多少系统有这么大的内存,所以CPU在设计时仅仅支持48位地址,可以少用很多硬件。如果未来系统需要扩展,则无须变更指令集,只要从硬件上扩展即可。

对于ZGC来说,由于多视图(也称为Color Pointer)的缘故,会额外占用4位地址位,所以真正可用的应该是44位。理论上ZGC最大可以支持16TB的内存,但是如果要扩展得更多,超过16TB时,则需要重新设计这一部分。

ZGC使用Color Pointer机制减少内存的访问,还需要解决一个问题,就是需要有一个机制来识别对象设置不同的地址位(即对象的地址不同),但是对象仍然处于同一个内存地址。这看起来非常怪异,一个对象是由唯一的地址确定的,但是目前需要有一个机制把多个地址和一个对象关联起来,只有这样的机制才能真正解决内存访问的问题。幸运的是,目前的OS都支持这样的方式,即多地址视图映射机制,把多个虚拟地址映射到一个物理地址上。

以JDK 11管理的4TB内存为例,按照图8-13的介绍,堆空间被划分为3个视图,分别是M0(即Marked0)、M1(即Marked1)和Remapped。这3个视图的地址布局如图8-15所示。

添加图片注释,不超过 140 字(可选)

图8-15 ZGC支持4TB内存地址空间设计

在ZGC中常见的虚拟空间有[0,4TB)、[4TB,8TB)、[8TB,12TB)、[16TB,20TB),其中[0,4TB)对应的是Java的堆空间;[4TB,8TB)、[8TB,12TB)、[16TB,20TB)分别对应M0、M1和Remapped这3个视图。最为关键的是M0、M1和Remapped这3个视图会映射到操作系统的同一物理地址。这几个空间的关系如图8-16所示。

添加图片注释,不超过 140 字(可选)

图8-16 虚拟地址和物理地址映射示意图

该图是ZGC在运行时虚拟地址和物理地址的转化。从图8-16中我们可以得到:

1)4TB是的堆空间,其大小受限于JVM参数。

2)0~4TB的虚拟地址是ZGC提供给应用程序使用的虚拟空间,它并不会映射到真正的物理地址。

3)操作系统管理的虚拟内存为M0、M1和Remapped这3个空间,且它们对应同一物理空间。

4)在ZGC中,这3个空间在同一时间点有且仅有一个空间有效。为什么这么设计?这就是ZGC的高明之处,利用虚拟空间换时间。这3个空间的切换由垃圾回收的不同阶段触发。

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

使用地址视图的好处就是加快标记和转移的速度。比如对于对象在标记阶段只需要转换地址视图。而地址视图的转化非常简单,只需要设置地址中第42~45位中相应的标志位即可。而在以前的垃圾回收器中,要修改对象的对象头,把对象头的标记位设置为已标记,这就会产生内存存取访问。而在ZGC中无须任何的对象访问。这就是ZGC在标记和转移阶段速度更快的原因。在标记过程中有一个技术细节值得注意:当对象被多个对象引用时,如何保证对象仅仅标记一次?下面通过一个简单的例子来演示这个问题,假定对象引入关系初始状态如图8-17所示。

添加图片注释,不超过 140 字(可选)

图8-17 对象引用关系初始状态

假设标记开始前地址视图为Remapped,GC工作线程将Obj1和Obj3标记,首先从一个视图(Remapped)映射到另外一个视图(M1)。此时Obj1和Obj3的地址视图为M1,而Obj2尚未完成标记,地址视图仍然为Remapped,并且Obj2中成员变量也没有更新,所以它指向的Obj3仍然是老的地址视图。也就是说,Obj1中指向的Obj3其地址位为M1+Address,Obj2指向的Obj3其地址位为Remapped+Address。部分对象标记后的地址视图如图8-18所示。

添加图片注释,不超过 140 字(可选)

图8-18 部分对象标记后视图信息

当Obj2完成标记后,其地址视图也变成了M1,但是Obj2指向的Obj3地址仍然为Remapped+Address。实际上Obj3已经通过Obj1的引用链完成了标记。

该如何处理Obj2中仍然指向的过时对象视图呢?示意图如图8-19所示。

添加图片注释,不超过 140 字(可选)

图8-19 对象引用过时的对象视图

由于Obj3已经被标记过,意味着Obj2指向过时对象Obj3,无须再次标记。所以对于Obj2处理时只需要让Obj2修正引用指针即可。注意,这里的修正和前文提到的因为对象转移后对象地址变化的修正稍有不同,这里进行修正,是因为标记过程对象地址视图不同。

处理方法是,通过Obj2的引用获得Obj3过时的指针,通过该指针访问oop对象。因为底层oop只有一个,所以此时获取的对象既可能反映Remapped视图,也可能反映M1视图,只要保证通过oop对象获得M1地址视图,就说明对象Obj3已经标记,无须再次标记。所以可以访问oop的成员,只要该成员能反映Obj3的地址视图就可以。在JVM中,oop有一个oopDesc信息(也就是对象头),oopDesc在oop的头部,所以可以通过oop获取oopDesc的地址,通过oopDesc的地址视图判断Obj3处于哪个视图中。相关伪代码如下:

inline uintptr_t ZOop::to_address(oop o) { return cast_from_oop<uintptr_t>(o); } template <class T> inline T cast_from_oop(oop o) { return (T)((oopDesc*)o); }

垃圾回收触发的时机

ZGC中采用了主动垃圾回收的方式,设计了一系列规则,只要系统运行时满足其中的一个规则就会触发并执行垃圾回收。设计的规则如下。

(1)基于固定时间间隔触发

该规则的目的是希望ZGC的垃圾回收器以固定的频率(或者时间间隔)触发。这在一些场景中非常有用,例如应用程序在请求量比较低的情况下运行了很长时间,但是ZGC不满足其他垃圾回收器的触发条件,所以一直不会触发垃圾回收,这通常没什么问题,但如果在某一个时间点开始请求暴增,则可能导致内存使用也暴增,而垃圾回收器来不及回收垃圾对象,这将降低应用系统的吞吐量。所以ZGC提供了基于固定时间间隔触发垃圾回收的规则。

这个规则的实现非常简单,就是判断前一次垃圾回收结束到当前时间是否超过时间间隔的阈值,如果超过,则触发垃圾回收,如果没超过,则直接返回。

需要说明的是,时间间隔由一个参数ZCollectionInterval来控制,这个参数的默认值为0,表示不需要触发垃圾回收。在实际工作中,可以根据场景设置该参数。

(2)预热规则触发

该规则是说,当JVM刚启动时,还没有足够的数据来主动(或者智能的)触发垃圾回收的启动,所以设置了预热规则,用于强制触发垃圾回收。

预热规则指的是JVM启动后,当发现堆空间使用率达到10%、20%和30%时,会主动触发垃圾回收。ZGC设计最多前3次垃圾回收由预热规则触发。也就是说,当垃圾回收触发(无论是由预热规则还是主动触发垃圾回收)的次数超过3次,预热规则将不再生效。

(3)根据分配速率

根据分配速率来预测是否能触发垃圾回收。这一规则设计的思路如下。

1)收集数据:在程序运行时,收集过去一段时间内垃圾回收发生的次数、执行的时间、内存分配的速率memratio和当前空闲内存的大小memfree。

2)计算:根据过去垃圾回收发生的情况预测下一次垃圾回收发生的时间timegc,按照内存分配的速率预测空闲内存能支撑应用程序运行的实际时间timeoom,例如timeoom = memfree/memratio。

3)设计规则:若timeoom小于timegc(垃圾回收的时间),可以启动垃圾回收。这个规则的含义是,如果从现在起到oom发生前开始执行垃圾回收,刚好在OOM发生前完成垃圾回收的动作,从而避免oom。在ZGC中ZDirector是周期性地运行的,所以在计算时还应该用oom的时间减去采样周期的时间。采样周期记为timeinterval,则规则为:当timeoom< timegc + timeinterval时触发垃圾回收。

那么任务就变成了如何预测下一次垃圾回收时间timegc和内存分配的速率memratio(因为memfree是已知数据,无须额外处理)。

下面以预测垃圾回收时间timegc为例来看看如何预测。最简单的想法是,根据已经发生的垃圾回收所使用的时间来预测下一次垃圾回收可能花费的时间。这里提供几种思路:

1)收集过去一段时间内垃圾回收发生的次数和时间,取过去N次垃圾回收的平均时间作为下一次垃圾回收的预测时间。这一方法最为直观,但是准确度可能不高。

2)收集过去一段时间内垃圾回收发生的次数和时间,建立一个逻辑回归模型,从而预测下一次垃圾回收的预测时间。这一方法虽然比第一种方法有改进,根据垃圾回收的趋势来预测下一次垃圾回收的时间,但这一方法最大的问题是逻辑回归模型太简单。实际上,如果我们能提供更多的输入,比如应用程序使用内存的情况、线程数等建立动态模型,这应该是一个非常好的方法。

3)使用衰减平均时间来预测下一次垃圾回收花费的时间。衰减平均方法实际上是第一种方法和第二种方法组合后的一种简化实现。它是一种简单的数学方法,用来计算一组数据的平均值,但是在计算平均值的时候最新的数据有更高的权重,即强调近期数据对结果的影响。衰减平均计算公式如下:

添加图片注释,不超过 140 字(可选)

式中α为历史数据权值,1-α为最近一次数据权值。即α越小,最新的数据对结果影响越大,最近一次的数据对结果的影响最大。不难看出,其实传统的平均就是α取值为(n-1)/n的情况。在G1中预测下一次垃圾回收时间采用的就是这种方法。

4)直接采用已经成熟的模型来预测下一次垃圾回收时间。ZGC中主要基于正态分布来预测。

学过概率论的读者都知道正态分布。简单回顾一下正态分布的相关知识。首先它是一条中间高、两端逐渐下降且完全对称的钟形曲线,如图8-20所示。

添加图片注释,不超过 140 字(可选)

图8-20 正态分布图

正态分布也非常容易理解,它指的是大多数数据应该集中在中间附近,少数异常的情况才会落在两端。

对于垃圾回收算法中的数据——内存的消耗时间和垃圾回收的时间也应该符合这样的分布。注意,并不是说G1中的停顿预测模型不正确或者效果不好,而是说使用正态分布来做预测有更强的数学理论支撑。在使用中,ZGC还对这个数学模型做了一些改变。

通常使用N表示正态分布,假设X符合均值为μ、方差为σ2的分布,做数学变换令Y=(X-μ)/σ,则它符合N(0, 1)分布。如下所示:

添加图片注释,不超过 140 字(可选)

正态分布有一些很好的数学特性,均值位于曲线的中间(见图8-20中虚线),当标准差σ=μ时,该区间的概率可以达到68.27%,即大多数情况下都位于该区间。

假设内存分配的时间符合正态分布,我们可以获得抽样数据,从而估算出内存分配所需时间的均值和方差。这个均值和方差是我们基于样本数据估算得到的,它们与真实的均值和方差相比可能有一定的误差。所以如果我们直接使用这个均值和方差,可能会因样本数据波动而出现不准确的情况,因此在概率论中引入了置信度和置信区间。简单地说,置信区间指的是这个参数估计的一个区间,区间是这个参数的真实值的一定概率落在测量结果周围的程度。而置信度指的就是这个概率。

假定给定一个内存分配花费的时间序列X1,X2,…,Xn,我们想要知道在99.9%的情况下内存分配花费的时间。方法如下:

已知点估计量服从的分布如下:

添加图片注释,不超过 140 字(可选)

其中μ为样本均值,σ为样本标准差。

对应99.9%置信度,查标准正态分布表得到统计量为3.290 527。所以可以得到99.9%的情况下内存分配花费的时间的概率为

添加图片注释,不超过 140 字(可选)

等价于

添加图片注释,不超过 140 字(可选)

由此可以得到置信区间为

添加图片注释,不超过 140 字(可选)

。可以得到最大的内存消耗在满足99.9%的情况下不会超过

添加图片注释,不超过 140 字(可选)

这个时间。在ZGC中对这个公式又做了一点修改,实际上是把这个值变得更大,对均值提供了一个参数,用于放大或者缩小均值,参数为ZAllocationSpikeTolerance,简单记为Tolerance,则公式为

添加图片注释,不超过 140 字(可选)

。Tolerance的默认值为2,这样的结果使得置信度更高,即远大于99.9%。

在ZGC中,内存分配的速率memratio的处理和timegc完全相同,从而ZGC利用正态分布完成预测,并利用预测的时间来设计触发垃圾回收的规则。这个规则应该是ZGC中最常见的垃圾回收触发规则。

在这里稍微提一下,从统计角度来说,当数据样本足够大的时候(比如样本个数大于30个时)使用正态分布比较准确;当样本个数不多时,使用t分布效果比较好。在上述代码中实际上修正了真正的置信区间,使得置信度更高。如果读者有兴趣,可以实现t分布,并验证t分布和正态分布预测的准确度。

(4)主动触发

该规则是为了实现应用程序在吞吐量下降的情况下,当满足一定条件时,还可以执行垃圾回收。这里的满足一定条件指的是:

1)从上一次垃圾回收完成到当前时间,应用程序新增使用的内存达到堆空间的10%。

2)从上一次垃圾回收完成到当前时间,已经过去了5分钟,记为timeelapsed。

如果这两个条件同时满足的话,预测垃圾回收时间为timegc,定义规则:

如果numgc×timegc < timeelapsed,则触发垃圾回收。其中numgc是ZGC设计的常量,假设应用程序的吞吐率从50%下降到1%后需要触发一次垃圾回收。

这个规则实际上是为了弥补程序吞吐率骤降且长时间不执行垃圾回收而引入的。有一个参数ZProactive用来控制是否开启和关闭主动规则,默认值是true,即默认打开主动触发规则。

实际上这个规则和第一个规则(基于固定时间间隔规则)在某些场景中有一定的重复,第一个规则只强调时间间隔,本规则除了时间之外还会考虑内存的增长和吞吐率下降的快慢程度。

(5)阻塞内存分配请求触发

阻塞内存分配由参数ZStallOnOutOfMemory控制,当参数ZStallOnOutOfMemory为true时进行阻塞分配,如果不能成功分配内存,则触发阻塞内存分配(该规则在JDK 17中被移除)。

(6)外部触发

外部触发是指在Java代码中显式地调用System.gc()函数,在JVM执行该函数时,会触发垃圾回收。该触发请求是从用户代码主动触发的,从编程角度来看,说明程序员认为此时需要进行垃圾回收(当然前提是程序员正确使用System.gc()函数)。所以ZGC把该触发规则设计为同步请求,只有在执行完垃圾回收后,才能执行后续代码。

(7)元数据分配触发

元数据分配失败时,ZGC会尝试进行垃圾回收,确保元数据能正确地分配。

异步垃圾回收后会尝试是否可以分配元数据对象空间,如果不能,则尝试进行同步垃圾回后是否可以分配元数据对象空间,如果还不成功,则尝试扩展元数据空间,若分配成功,则返回内存空间,不成功则返回NULL。

本篇文章给大家讲解的内容是JVM垃圾回收器详解:ZGC,回收设计

  1. 下篇文章给大家讲解的内容是JVM垃圾回收器详解:ZGC,垃圾回收实现

  2. 感谢大家的支持!

                                                                                                 资源获取:
    大家 点赞、收藏、关注、评论啦 、 查看
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

大牛用十年功力带你彻底理解JVM垃圾回收器:ZGC,回收设计 的相关文章

  • JVM HotSpot 上的 Java 异常计数器

    我想知道是否可以在不更改应用程序代码的情况下记录 JVM 级别上发生的每个异常 我所说的每个异常是指捕获和未捕获的异常 我想稍后分析这些日志并按异常类型 类 对它们进行分组 并简单地按类型对异常进行计数 我正在使用热点 也许有更明智的理由这
  • 使用 + 符号连接字符串

    今天我在读书Antonio 关于 toString 性能的博客 https antoniogoncalves org 2015 06 30 who cares about tostring performance 还有一段话 昨天曾经被认为
  • 热点 JVM 字节码解释器是跟踪 JIT 吗?

    这个问题几乎说明了一切 我一直在寻找答案 甚至通过 VM 规范 但我没有明确说明 No 不过 还有一些其他 JVM 具有跟踪 JIT HotPath http HotPath GoogleCode Com and Maxine http L
  • 我的代码中出现内存不足异常

    作为 Oracle 数据库压力测试的一部分 我正在长时间运行代码并使用 java 版本 1 4 2 简而言之 我正在做的是 while true Allocating some memory as a blob byte data new
  • 如何查看JVM中JIT编译的代码?

    有什么方法可以查看 JVM 中 JIT 生成的本机代码吗 一般用法 正如其他答案所解释的 您可以使用以下 JVM 选项运行 XX UnlockDiagnosticVMOptions XX PrintAssembly 根据特定方法进行过滤 您
  • 非活动状态下的 Spring Boot 堆使用情况

    我在本地部署了一个非常简单的 spring boot 应用程序 它只有一个类 控制器 差不多就这样了 我注意到堆分配并不稳定 并且有峰值和突然下降 为什么会这样 我没有对应用程序进行过一次调用 A view from VisualVM 事实
  • 什么触发了java垃圾收集器

    我对 Java 中垃圾收集的工作原理有点困惑 我知道当不再有对某个对象的实时引用时 该对象就有资格进行垃圾回收 但是如果它有对实时对象的引用怎么办 可以说我有一个节点集合 它们再次引用更多节点 List 1 gt Node a gt Nod
  • 在intellij中为java启用ssl调试

    从我的问题开始 上一期尝试通过 tls ssl 发送 java 邮件 https stackoverflow com questions 39259578 javamail gmail issue ready to start tls th
  • JVM 是否会内联对象的实例变量和方法?

    假设我有一个非常紧密的内部循环 每次迭代都会访问和改变一个簿记对象 该对象存储有关算法的一些简单数据 并具有用于操作它的简单逻辑 簿记对象是私有的和最终的 并且它的所有方法都是私有的 最终的和 inline 下面是一个示例 Scala 语法
  • Scala 对大数的阶乘有时会崩溃,有时不会

    以下程序经过编译和测试 有时返回结果 有时充满屏幕 java lang StackOverflowError at scala BigInt apply BigInt scala 47 at scala BigInt equals BigI
  • 如何判断我是在 64 位 JVM 还是 32 位 JVM 中运行(在程序内)?

    如何判断应用程序运行的 JVM 是 32 位还是 64 位 具体来说 我可以使用哪些函数或属性来在程序中检测到这一点 对于某些版本的 Java 您可以使用标志从命令行检查 JVM 的位数 d32 and d64 java help d32
  • 当目标是属性时,@Throws 不起作用

    在看的同时这个问题 https stackoverflow com q 47737288 7366707 我注意到申请 Throws to a get or setuse site 没有影响 此外 唯一有效的目标 for Throws ar
  • 在进行堆转储后,如何在发生 OutOfMemoryError 时重新启动 JVM?

    我知道关于 XX HeapDumpOnOutOfMemoryError https stackoverflow com q 542979 260805JVM 参数 我也知道 XX OnOutOfMemoryError cmd args cm
  • jvm 次要版本与编译器次要版本

    当运行使用具有相同主要版本但次要版本高于 JVM 的 JDK 编译的类时 JVM 会抛出异常吗 JDK 版本并不重要 类文件格式版本 http blogs oracle com darcy entry source target class
  • Scala 为了在 JVM 上运行做出了哪些妥协?

    Scala 是一种很棒的语言 但我想知道如果它有自己的运行时 如何改进 IE 由于 JVM 的选择 做出了哪些设计选择 我所知道的两个最重要的妥协是 类型擦除 http java sun com docs books tutorial ja
  • 使用 libjvm.so 时出现 Sigsegv Java 致命错误

    我正在做重启测试Sles12sp2 using STAF v3 4 24一段时间后我收到此错误 A fatal error has been detected by the Java Runtime Environment SIGSEGV
  • Android java.exe 以非零退出值 1 结束 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我尝试过查看类似的解决方案 但没有解决方案有效 我以前运行应用程序没有问题 但我的新应用程序突然开始给我带来问题 当我尝试运行它时总是
  • 将 Kotlin .kt 类打包到 JAR 中

    我如何构建HelloWorld kt as a JAR以便它运行 thufir dur kotlin thufir dur kotlin kotlinc HelloWorld kt include runtime d HelloWorld
  • Java:为什么.class文件中的方法类型包含返回类型,而不仅仅是签名?

    class 文件的常量池中有一个 NameAndType 结构 它用于动态绑定 该类可以 导出 的所有方法都被描述为 签名 返回类型 喜欢 getVector Ljava util Vector 当某些 jar 中方法的返回类型发生更改时
  • Java:VM 如何在 32 位处理器上处理 64 位“long”

    JVM 如何在 32 位处理器上处理 64 位的原始 long 在多核 32 位机器上可以并行利用多个核心吗 64 位操作在 32 位机器上慢了多少 它可能使用多个核心来运行不同的线程 但不会并行使用它们进行 64 位计算 64 位长基本上

随机推荐

  • 多语言vue-i18n (vue2)

    1 安装 vue i18n npm install vue i18n 8 save npm install vue i18n save 9版本需要vue3 0 在vue2环境下 默认安装 npm install vue i18n 的版本是
  • mysql数据库优化方式

    引言 如何比较两个相同类型产品的好坏 在功能大致相同的情况下 剩下的就拼的是速度 比谁的代码优化的更好 提高代码质量 优化数据库 优化SQL语句提高查询效率 采用Nosql等等 这次我要记录的是8种mysql数据库优化方式 1 选取最适用的
  • 【OpenCV学习笔记】【编程实例】一(VS2010/MFC+OPenCV2.4.7读取图片和摄像头)

    问题 VS2010 MFC OpenCV 显示图片 链接 http jingyan baidu com article f71d60375ddd411ab641d1e3 html 说明 可以了解一个基本的应用OpenCV的MFC程序的基本流
  • IDEA单元测试控制台不能输入数据

    问题 在使用IDEA写单元测试方法 其中包含了输入语句 但在控制台无法输入数据 解决方案 其实这个是IDEA本身的配置问题 我们只需要修改IDEA 添加相应配置文件就好啦 具体操作如下 选择最上方工具栏中的Help gt Edit Cust
  • git pull时报错:Your local changes to the following files would be overwritten by merge

    1 产生原因 这种情况一般是由于 你在工作区修改了一些代码 但是还不打算提交 这时同步主干代码的时候 如果你修改的文件与主干分支的代码有冲突 那么拉取主干分支代码时就会报这个错误 2 简单示例 下面我通过一个简单的例子给大家展示一下这个报错
  • 正弦积分与余弦积分函数

    文章目录 一 被积分函数分析 二 积分函数分析 参考 在看天线理论时遇到了正弦积分与余弦积分函数 为了更好的理解这个函数的性质 使用Python将这两个函数的图像复现出来 同时画了被积函数的图像 图1 书上的公式示例 正弦积分函数 S i
  • mysql 实现批量添加和更新功能

    mapper xml文件配置 单独添加
  • 计算机毕业设计springboot基于Hadoop平台的电影推荐系统541039【附源码+数据库+部署+LW】

    本项目包含程序 源码 数据库 LW 调试部署环境 文末可获取一份本项目的java源码和数据库参考 系统的选题背景和意义 选题背景 随着互联网的快速发展和大数据技术的成熟应用 电影推荐系统成为了电影行业中不可或缺的一部分 基于Hadoop平台
  • opengl es 3D要素——贴图

    前不久开始了正式的opengl es学习 目前为止 成就有 显示一些三维元素 一些可以自由旋转 一些可以用手指控制左右上下的旋转 下一步准备开始写光照了 在此之前先整理下思路 我的工程已经开始变得庞大了 需要声明下 我不太会把全部的代码贴出
  • 学计算机专业的打字要快么,怎样学电脑打字最快电脑新手如何快速学会打字

    01 在零基础的情况下 通过自学快速掌握键盘打字也是需要下一定的功夫的 首先需要说明的是 键盘上有很多按键 一个键盘具有许多的功能 分为了不同的区域 平常使用最多的是主键盘区 而在电脑上打字使用最多的是主键盘区里标有英文大写字母的26个键
  • javascript中的数据类型转换

    个人简介 作者简介 大家好 我是阿牛 个人主页 馆主阿牛 支持我 点赞 收藏 留言 系列专栏 web开发 格言 迄今所有人生都大写着失败 但不妨碍我继续向前 javascript中的数据类型转换 个人简介 前言 数值型转换为字符串类型 字符
  • 如何通过本地搭建wamp服务器并实现无公网IP远程访问

    文章目录 前言 1 Wamp服务器搭建 1 1 Wamp下载和安装 1 2 Wamp网页测试 2 Cpolar内网穿透的安装和注册 2 1 本地网页发布 2 2 Cpolar云端设置 2 3 Cpolar本地设置 3 公网访问测试 4 结语
  • Linux系统下串口调试助手putty,cutecom,xgcom

    系统 linux mint 18 3 xfce 64bit 串口驱动 ch340 系统已自带 没有测试pl2302 尝试了几个linux下的串口调试助手 这里总结一下 由于使用的是USB转串口 CH340 所以在 dev 目录下面看到的串口
  • 人脸识别经典算法三:Fisherface(LDA)

    Fisherface是由Ronald Fisher发明的 想必这就是Fisherface名字由来 Fisherface所基于的LDA Linear Discriminant Analysis 线性判别分析 理论和特征脸里用到的PCA有相似之
  • [杂乱笔记]algorithm头文件下的常用函数

    使用algorithm头文件 需要在头文件加using namespace std 1 常见的函数 max min abs swap int x 98 int y 7 int z 12 printf d d n max x y min y
  • 详细解剖大型H5单页面应用的核心技术点

    阐述下项目 Xut js 开发中一个比较核心的优化技术点 这是一套平台代码 并非某一个插件功能或者框架可以直接拿来使用 核心代码大概是6万行左右 不包含任何插件 这也并非一个开源项目 不能商业使用 只是为了作者开发方便同步修改代码而上传的源
  • FISCO-BCOS 八、ubuntu配置Java环境

    ubuntu配置Java环境需根据自己的java版本号进行细节修改 本文以jdk11 0 18版本为例 一 安装Java 安装默认Java版本 Java 8或以上 sudo apt install y default jdk 查询Java版
  • GPFS文件系统分析

    什么是GPFS文件系统 GPFS General Parallel File System 是IBM公司开发的高性能集群文件系统 从1998年 开始首先应用于AIX集群 2001年后应用于Linux集群 在集群的环境中 GPFS文件系统允许
  • 关于路由, 我好奇的那些点

    最近解锁了一个拯救自我的新技能 就是学习之前 可以先观察下知识时间线 相关人物 以及其他背景等 让那些规则不再变得辣么无聊 什么是路由 通俗的讲就是根据不同的url展示不同页面或者内容 路由的发展阶段 路由的概念最开始是在后端出现的 在以前
  • 大牛用十年功力带你彻底理解JVM垃圾回收器:ZGC,回收设计

    回收设计 ZGC的并发回收算法采用的也是 目的空间不变性 的设计 关于目的空间不变性的更多内容可以参考第7章 在第7章中提到 Shenandoah从JDK 13开始也采用 目的空间不变性 的设计 但是ZGC与Shenandoah相比 还是有