垃圾回收算法手册读书笔记(七)并行垃圾回收

并行垃圾回收

到目前为止,我们讨论的垃圾回收都是假定存在多个赋值器线程,但只存在一个回收器线程。这显然对于现代多核或多处理器而言,是极大的资源浪费。如果我们能够将垃圾回收并行化,将明显的提升垃圾回收器的性能。

我们需要注意的是,并行回收仍然需要在回收的过程中stop the world,即挂起所有赋值器线程,在此期间使用多个线程并行执行垃圾回收任务。

图中可以看出并行回收与之前讨论的单线程回收之间的差异,仅仅在于垃圾回收过程的并行化。之前我们讨论回收基本算法的时候已经知道了,除去引用计数法,垃圾回收的主要过程包括

  • 标记
  • 清扫
  • 复制
  • 整理

因此并行垃圾回收主要讨论的是以上四个过程的并行化

是否有足够多的工作可以并行

在讨论并行化的时候,首先要确定的就是当前的工作能否并行。

某些垃圾回收问题的出现可能不利于使用并行回收。例如当标记-清扫回收器在对链表进行追踪时,链表固有的顺序特征决定了再追踪阶段的每一步中,标记栈中的仅会包含一个元素,即链表中下一个将被追踪的节点。在这一场景下,只有一个回收线程处于工作状态,其他线程均处于等待。

在包含p个处理器的系统中,由于缺少工作而处于等待状态的标记线程数量n与任意可达对象的最大深度之间的关系为:n <= (p-1) * max depth(o) ,max depth(o) 为对象引用的最大深度。

同时我们还需要考虑,并行化解决方案引入的额外开销(比如线程之间的同步),是否小于并行带来的收益

负载均衡

并行化还需要考虑的就是,任务在多个线程(处理器)上的均衡问题。如果这个问题无法解决,很容易出现”一核有难,七核围观”的情况,从而使并行化效果大打折扣。

负载均衡方案大致分为静态负载均衡动态负载均衡

静态负载均衡

静态负载均衡要求并行化任务在一开始就划分好,比如在并行标记阶段,一开始将回收器的根对象划分成多个等量的集合,每个线程负责一个集合内根的追踪。静态负载均衡方案下线程之间同步开销较小,但是很难达到任务分配均匀。就对象标记阶段而言,其工作量的大小取决于存活对象的数量,而每个根集合所引用的存活对象数量一开始是无法得知的。

动态负载均衡

动态负载均衡则不要求一开始就划分好任务,而是在执行的过程中动态的进行。动态负载均衡算法通常可以抽象成以下三种操作:

  1. 获取工作
  2. 执行工作
  3. 生成新工作

算法一般配备一个全局的工作池,以供工作线程不断的从中获取并执行任务,同时将任务分解成更小的子任务加入到全局工作池中。如果任务能够划分到足够细的粒度,那么这种方案就能达到近似的均衡。

更进一步,为了避免在全局工作池上过多的同步工作,可以使用工作窃取的策略,为每个线程配备一个本地工作列表,每个线程将新任务添加入本地工作列表,并且在本地工作列表为空时从其他线程的本地工作列表中”窃取”一个任务

同时我们还需要考虑,任务需要划分的粒度到底多细合适,粗粒度的任务不利于任务的负载均衡,而细粒度的任务则需要更大的同步开销(多个线程间在全局工作池中同步的获取与添加任务)

并行回收算法

并行垃圾回收算法大致可以分为两类,以处理器为中心以内存为中心的并行算法。以处理器为中心的算法通常关注与任务在各线程之前的分配,而不关注所处理的对象在内存中的位置。但我们也了解到,即使是单处理器,局部性都会对性能产生显著影响,以内存为中心的算法通常会考虑对堆中连续内存进行操作。

书中较为详细的讨论了标记,清扫,整理,复制的并行化实现,有兴趣可以直接看书(不然篇幅太多写不完啊 ◐▽◑)这里仅拿出并行标记进行简单讨论。

并行标记

目前已知的并行标记算法都是以处理器为中心,标记过程主要分为3步:

  • 获取一个对象(获取任务)
  • 检查并设置标记位(执行任务)
  • 将对象的子节点加入工作列表(生成新任务)

基于上述三步操作,我们可以设计出基于动态负载均衡的并行标记算法,比如基于工作窃取的算法。但是我也应该了解到,并行标记的负载均衡依赖于对象图的结构,如果对象图的深度太深将不利于并行标记