并行垃圾回收
到目前为止,我们讨论的垃圾回收都是假定存在多个赋值器线程,但只存在一个回收器线程。这显然对于现代多核或多处理器而言,是极大的资源浪费。如果我们能够将垃圾回收并行化,将明显的提升垃圾回收器的性能。
我们需要注意的是,并行回收仍然需要在回收的过程中stop the world,即挂起所有赋值器线程,在此期间使用多个线程并行执行垃圾回收任务。
图中可以看出并行回收与之前讨论的单线程回收之间的差异,仅仅在于垃圾回收过程的并行化。之前我们讨论回收基本算法的时候已经知道了,除去引用计数法,垃圾回收的主要过程包括
- 标记
- 清扫
- 复制
- 整理
因此并行垃圾回收主要讨论的是以上四个过程的并行化
是否有足够多的工作可以并行
在讨论并行化的时候,首先要确定的就是当前的工作能否并行。
某些垃圾回收问题的出现可能不利于使用并行回收。例如当标记-清扫回收器在对链表进行追踪时,链表固有的顺序特征决定了再追踪阶段的每一步中,标记栈中的仅会包含一个元素,即链表中下一个将被追踪的节点。在这一场景下,只有一个回收线程处于工作状态,其他线程均处于等待。
在包含p个处理器的系统中,由于缺少工作而处于等待状态的标记线程数量n与任意可达对象的最大深度之间的关系为:n <= (p-1) * max depth(o) ,max depth(o) 为对象引用的最大深度。
同时我们还需要考虑,并行化解决方案引入的额外开销(比如线程之间的同步),是否小于并行带来的收益
负载均衡
并行化还需要考虑的就是,任务在多个线程(处理器)上的均衡问题。如果这个问题无法解决,很容易出现”一核有难,七核围观”的情况,从而使并行化效果大打折扣。
负载均衡方案大致分为静态负载均衡与动态负载均衡
静态负载均衡
静态负载均衡要求并行化任务在一开始就划分好,比如在并行标记阶段,一开始将回收器的根对象划分成多个等量的集合,每个线程负责一个集合内根的追踪。静态负载均衡方案下线程之间同步开销较小,但是很难达到任务分配均匀。就对象标记阶段而言,其工作量的大小取决于存活对象的数量,而每个根集合所引用的存活对象数量一开始是无法得知的。
动态负载均衡
动态负载均衡则不要求一开始就划分好任务,而是在执行的过程中动态的进行。动态负载均衡算法通常可以抽象成以下三种操作:
- 获取工作
- 执行工作
- 生成新工作
算法一般配备一个全局的工作池,以供工作线程不断的从中获取并执行任务,同时将任务分解成更小的子任务加入到全局工作池中。如果任务能够划分到足够细的粒度,那么这种方案就能达到近似的均衡。
更进一步,为了避免在全局工作池上过多的同步工作,可以使用工作窃取的策略,为每个线程配备一个本地工作列表,每个线程将新任务添加入本地工作列表,并且在本地工作列表为空时从其他线程的本地工作列表中”窃取”一个任务
同时我们还需要考虑,任务需要划分的粒度到底多细合适,粗粒度的任务不利于任务的负载均衡,而细粒度的任务则需要更大的同步开销(多个线程间在全局工作池中同步的获取与添加任务)
并行回收算法
并行垃圾回收算法大致可以分为两类,以处理器为中心和以内存为中心的并行算法。以处理器为中心的算法通常关注与任务在各线程之前的分配,而不关注所处理的对象在内存中的位置。但我们也了解到,即使是单处理器,局部性都会对性能产生显著影响,以内存为中心的算法通常会考虑对堆中连续内存进行操作。
书中较为详细的讨论了标记,清扫,整理,复制的并行化实现,有兴趣可以直接看书(不然篇幅太多写不完啊 ◐▽◑)这里仅拿出并行标记进行简单讨论。
并行标记
目前已知的并行标记算法都是以处理器为中心,标记过程主要分为3步:
- 获取一个对象(获取任务)
- 检查并设置标记位(执行任务)
- 将对象的子节点加入工作列表(生成新任务)
基于上述三步操作,我们可以设计出基于动态负载均衡的并行标记算法,比如基于工作窃取的算法。但是我也应该了解到,并行标记的负载均衡依赖于对象图的结构,如果对象图的深度太深将不利于并行标记