4000字搞懂并行回收内存管理模型+NUMA支持+内存分配和GC触发流程

并行回收

并行回收(Parallel Scavenge Garage Collection,简称ParallelGC、PS GC或PS)是JVM吞吐率最高的垃圾回收器之一。

并行回收也采用分代内存管理方式,和串行回收采用的算法基本一致,但在实现时有不少特色,主要表现如下:

1)具有独特的内存管理机制,不仅支持新生代中Eden和Survivor大小的自适应调整,还支持两个代大小的自适应调整。

2)它是第一个支持NUMA-Aware的垃圾回收器,并且可以根据应用的运行情况自适应地调整NUMA所用的大小。

3)新生代采用并行复制算法,类似于第4章的ParNew;如果在Minor GC发生后仍然无法满足Mutator的内存请求,则会采用并行标记压缩算法对整个内存进行回收,算法的并行化基于依赖树结构实现。

下面着重介绍并行回收这3个特点。

内存管理

并行回收的内存管理与JVM中其他的垃圾回收器的内存管理都不相同。其主要原因是并行回收希望在运行时根据Mutator运行的情况动态地调整新生代和老生代的边界。

根据内存分代后每个代的使用原则,Mutator运行时在新生代中分配对象,当新生代空间不足时执行Minor GC,并将长期存活的对象晋升到老生代。

分代的代际边界可以调整,意味着:新生代可以增大/减少,而老生代可以减少/增大。但无论边界如何调整,都不应该移动老生代的对象,否则边界调整将导致性能下降(或者表现为停顿时间增加)。

另外,新生代和老生代都是连续的虚拟地址,所以需要小心设计内存管理模型,既能满足代际边界的动态调整,又能满足空间的连续分配。通常有两种内存模型可以满足上述要求,分别是:

1)新生代在前,老生代在后。

2)老生代在前,新生代在后。

对于第一种内存模型,新生代的内存分配方向是从低地址往高地址分配,而老生代的内存分配方向是从高地址往低地址分配,如图5-1所示。

图5-1 新生代在前,老生代在后的内存管理模型

使用该模型,当执行完Minor GC后判断是否需要调整边界,如果需要,可以老生代使用的低地址作为边界,并以此边界作为新生代大小的上界控制新生代大小的调整。该模型的优点是新生代的管理和其他垃圾回收器的内存管理模型一致,但是在老生代中分配内存需要从高地址往低地址方向分配。

另外,该模型对于Full GC的实现稍微有些复杂,在Full GC发生时需要将所有对象从高地址开始压缩、整理。

对于第二种内存模型,老生代的内存分配方向为从低地址往高地址分配,而新生代的内存分配方向为从高地址往低地址分配,如图5-2所示。

图5-2 老生代在前,新生代在后的内存管理模型

使用该模型,当执行完Minor GC后判断是否需要调整边界,如果需要,则可以根据老生代使用的高地址作为边界,并以此边界作为新生代大小的下界控制新生代大小的调整。该模型的优点是对Full GC友好。使用该模型,新生代地址从高地址往低地址方向分配,但由于内存分配使以总是以低地址作为起始位置,因此在分配内存时需要先计算内存大小,然后根据使用内存的边界计算出低地址的起始位置。

使用这两种模型都可以满足调整新生代的大小的需求,也可以满足在Minor GC晋升对象后动态调整老生代大小的需求。

内存管理模型

使用第二种模型实现的复杂度较低,所以并行回收内存模型是按照老生代在前、新生代在后的组织方式实现的。

对于一些应用来说,并不需要边界调整这个功能。例如,应用新生代占用的内存相对固定,此时如果采用固定边界,新生代和老生代在内存分配时都可以从低地址往高地址分配,从而减少新生代内存分配时额外的地址调整操作。

在并行回收中通过参数UseAdaptiveGCBoundary(默认值为false,表示不支持边界浮动功能)控制应用是否需要支持浮动边界功能。由于并行回收同时支持边界固定和边界浮动的功能,为了保存代码统一,在边界固定的场景中也是老生代在前、新生代在后。边界固定的内存管理模型如图5-3所示。

图5-3 边界固定的内存管理模型

而边界浮动的内存模型如图5-2所示,但是该特性在JDK 15中被移除,主要原因是边界浮动可能导致一些应用崩溃,而且该功能使用得较少,并且JVM的重心不再是并行回收,因此JVM开发团队直接移除了该功能。

另外,在并行回收新生代中支持自适应调整Eden和Survivor分区的大小。该功能可以通过参数UseAdaptiveSizePolicy(默认值为true,表示支持调整)控制是否开启。但是该功能也在一定程度上增加了内存管理的复杂性。根据复制算法的实现,将新生代划分为3个空间,分别是Eden、To和From,如图5-4所示。

图5-4 新生代的空间划分

在复制算法执行结束后,To空间中保存的是新生代中的所有活跃对象(已经晋升的对象除外),Eden和From空间都为空。其中Eden用于下一次Mutator对象分配,From和To空间交换,交换后To空间为空,To用于保存下一次复制算法发现的活跃对象。此时新生代的内存布局如图5-5所示。

图5-5 新生代的内存布局

由于From空间保存的是活跃对象,当调整From和To空间的大小时,要避免移动From空间中的对象。所以在调整From和To空间大小的时候需要考虑To和From这两个空间的顺序。

如果垃圾回收后子空间的顺序为Eden、To和From,那么无论是扩大还是缩小From和To空间的大小,调整方式都比较简单。只需要保证调整后Eden+To的大小小于From的起始地址即可。

如果垃圾回收后空间的顺序为Eden、From和To,那么由于From空间有存活对象,需要保证Eden的结束地址小于From的起始地址(否则必须移动From空间中的对象),同时要求From空间调整后的大小大于现有存活对象的总大小。在该顺序下调整From和To空间的大小,在调整后可能导致内存有空洞(即有部分内存无法使用),例如将From的起始地址调整到比当前地址小的起始地址后(仅仅修改起始地址,但不移动对象),此时新的起始地址和老的起始地址之间就形成了空洞。

在复制算法的执行过程还可能会遇到转移失败的情况,即To空间和老生代都没有足够的空间来保存新生代中标记的活跃对象。对于这种情况,一定有Eden和To空间不为空,说明发生了转移失败的情况。在这种情况下不会调整新生代子空间的大小,也就是说只有成功执行复制算法的垃圾回收才可能尝试调整新生代子空间的大小。

NUMA支持

并行回收是第一个支持NUMA-Aware的垃圾回收器。在Mutator请求内存的时候,如果打开UseNUMA特性,可以从Mutator运行的节点分配内存。使用NUMA-Aware的方式管理内存,可以加速Mutator对内存的访问,但是需要考虑多个NUMA节点共享一个Eden大小时该如何触发垃圾回收的问题。一个常见的处理方法是“任意”一个NUMA节点的内存不足时,都会触发垃圾回收。这样的触发方式最为简单,但是每个NUMA节点上Mutator请求的内存并不相同,可能存在差异比较大的情况。所以并行回收设计了两种方法来管理新生代内存:

1)每一个NUMA节点平分Eden的大小。

2)每一个NUMA节点根据Mutator使用的内存大小,动态地为每一个NUMA节点分配内存。

默认方式是,多个NUMA节点(例如共有n个NUMA节点)平均划分Eden的空间,例如系统有4个NUMA节点,整体Eden为1GB,则每个NUMA节点管理的内存上限为256MB,当NUMA节点上内存使用达到256MB时就会触发垃圾回收。NUMA平分Eden的管理方式如图5-6所示。

图5-6 Eden的NUMA管理方式

而实际情况是,每个节点使用内存的速率并不相同,所以并行回收支持动态地调整NUMA节点的内存大小。并行回收提供一些参数来控制每个NUMA管理内存的大小,分别是:

1)参数
UseAdaptiveNUMAChunkSizing,控制是否可以动态调整NUMA节点的内存大小。参数默认值为true,表示支持动态调整每个NUMA节点管理内存的大小。

2)参数
AdaptiveSizePolicyReadyThreshold,当收集数据的次数达到该阈值时才会调整NUMA节点的大小。参数默认值为5,表示只有发生过5次MinorGC才会启动动态调整NUMA节点大小的功能(该参数为开发参数,生产版本的JDK不能调整该参数)。

3)参数NUMAChunkResizeWeight,计算分配速率时使用。参数默认值为20,表示参数调整时历史数据的权重。

4)参数NUMASpaceResizeRate,用于控制调整NUMA节点内存时的步幅。

该参数可防止某一个NUMA过多分配Eden。例如参数默认值为1GB,如果系统有4个NUMA节点,则步幅控制可以通过公式

来计算。

内存分配和GC触发流程

一般来说,新生代主要响应Mutator的分配请求,老生代用于Minor GC晋升对象的分配。但是如果新生代和老生代划分不合理,则可能存在频繁触发Minor GC而老生代仍然还有非常大的空间的情况。为此,并行回收尝试在新生代无法满足Mutator的分配请求时,在满足一定条件时,在老生代中响应Mutator的内存请求。

在并行回收中每个Mutator也都有一个TLAB,内存请求先从TLAB中分配,当TLAB无法满足Mutator的响应时,会尝试从新生代中分配一块新的TLAB(注意,如果Mutator请求的内存大小大于TLAB,会按照实际内存直接在新生代中分配)。

与3.2节介绍的分配顺序稍有不同,在3.2节介绍串行回收和CMS的分配分为3个层次——无锁分配、加锁分配、垃圾回收后分配,分配的成本依次增加。在并行回收中,分配增加了一个层次,共分为4个层次:无锁分配、加锁分配、在老生代中分配、垃圾回收后分配。当然,在老生代中的分配尝试需要满足一定条件才可以进行。在老生代中分配的流程如图5-7所示。

图5-7 额外增加的老生代分配策略

执行垃圾回收后内存分配的流程图如图5-8所示。

图5-8 垃圾回收后内存分配流程图

值得注意的是,并行垃圾回收中Full GC处理稍有不同,在Minor GC执行结束后,会判断是否需要执行Full GC,如果需要,则执行Full GC(至于是否要清除引用,则依赖于引用的状态)。所以一次Minor GC可能会触发3次Full GC(其中两次Full GC是Minor GC中的正常流程,第一次是Full GC不回收Java引用,第二次是Full GC回收Java引用,第三次是GC执行完成后额外触发的Full GC)。在以下两种情况下Minor GC会额外触发第三次Full GC。

1)Minor GC执行失败,即老生代无法满足新生代晋升的需要。

2)预测的晋升对象大小大于老生代可用的内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值