参考资料《代码的未来》(作者: [日] 松本行弘)。
本文是真对《GC的三种基本实现方式》文章中所提到的三种基本实现方式的基础上进行优化改良的方案/原理讲解
由于并非本人原著(我只是个“搬运工“),SO 未经本人允许请尽情转载。
GC的基本算法,大体上逃不出上述三种方式以及他们的衍生品。现在通过着三种方式进行融合,出现了一些更加高级的方式,这里将介绍最具代表性的三种,即:
有些情况下也可以对以上三种方法种的几种进行组合使用。
由于GC和程序处理的本质是无关的,因此它们所消耗的时间越短越好。分代回收的目的正是为了在程序运行期间,将GC所消耗的时间尽量缩短。
分代回收的基本思路是利用了一半性程序所具备的性质,即大部分对象都会在短时间内成为垃圾,而经过一定时间依然存活的对象往往拥有较长的寿命。如果寿命长的对象更容易存活下来,寿命短的对象则会被很快废弃,那么到底怎样做才能让GC变的更加高效呢?如果对分配不久——诞生时间较短的“年轻”对象进行重点扫描,应该就可以更有效的回收大部分垃圾。
在分代回收中,对象按照生成的时间进行分代,刚刚生成不久的年轻对象划为新生代(Young generation),而存活了较长时间的对象划为老生代(Old generation)。根据具体实现的方式不同,可能还会划分为更多不同的“代”,而这里为了讲解方便仅限定为“两代”。如果上述关于对象寿命的假说成立,那么只要仅仅扫描“新生代”对象,就可以回收废弃对象中很大一部分。
像这种只扫描新生代对象的回收操作,被称为小回收(Minor GC)。小回收的具体回收步骤如下:
首先从根开始一次常规扫描,找到“存活”对象。这个步骤采用标记清除或者复制收集算法都可以,不过大多数分代回收的实现都采用了复制收集的算法。需要注意的是,在扫描的过程中,如果遇到属于“老生代”的对象,则不对该对象进行递归扫描。这样一来,需要扫描的对象数量就会大幅减少。
然后,将第一次扫描后残留下来的对象划分到“老生代”。具体来说,如果是用复制收集算法的话,只要将复制目标的空间设置为老生代就可以了;而标记清除算法的话,则大多采用在对像上设置某种标志的方式。
从老生代对象对新生代对象的引用怎么办呢?如果只扫描新生代区域的话,那么从老生代对新生代的引用就不会被检测到。这样以来,如果一个年轻的对象只有来自老生代对象的引用,就会被误认为已经“死亡”了。
因此,在分代回收中,会对对象的更新进行监视,将从老生代对新生代的引用记录在一个叫做记录集的表中(如下图)。在执行小回收的过程中,这个记录集也将作为一个根来对待。
注意:图中可以看到老生代中任何地方都没有进行引用对象F,而F则会在“大回收”中被回收。对象C则在小回收中被回收。
要让分代回收正确工作,必须使记录集的内容保持更新。为此,在老生代到新生代的引用产生瞬间,就必须引用记录,而负责执行这个操作的子程序,需要被嵌入到所有设计对象更新操作的地方。
负责记录的子程序工作流程为:
假设有两个对象——A和B,当对A的内容进行改写并引入B时,判断如果A为老生代对象,且B为新生代对象则将该引用添加到记录集中。
这种检查程序需要对所有涉及修改对象内容的地方进行保护,因此被称为写屏障(Write barrier)。写屏障不仅用于分代回收,同时也用在很多其他的GC算法中。
随着程序的运行老生代中的“死亡”对象也在不断增加。为了避免这些对象占用内存空间,偶尔需要对包括老生代区域在内的所有对象进行扫描回收。像这样对所有区域的对象进行GC操作被称为全回收(Full GC)后者大回收(Major GC)。
分代回收通过减少GC中的扫描对象数量,达到缩短GC带来的平均中断时间的效果。不过由于还是需要进行大回收,因此最大中断时间并没有得到改善。从吞吐量来看,在对象寿命假说成立的程序中,由于 扫描对象的数量减少,可以到到非常不错的成绩。但是,其事迹性能会被程序行为,分代数量,大回收出发条件等因素大幅度左右。
在对实时性要求很高的程序中,缩短GC的最大中断时间尤其重要。比如汽车控制程序,或者机器人控制程序……如果因为GC操作而让控制程序中断0.1秒的时间后果都是不堪设想的。
在这些对实时性要求很高的程序中,必须能够对GC产生中断时间做出预测,例如可以将“最多只能中断10毫秒”作为附加条件。
然而在一般的GC算法中,做出这样的保证是不可能的,因为GC产生的中断时间于对象的数量和状态有关。因此,为了维持程序的实时性,不等到GC全部完成,而是将GC操作细分成多个部分逐一执行。这种方式被称为增量回收。
在增量回收中,由于GC过程是渐进的,在回收过程中程序本身会继续运行,对象之间的引用关系也可能会发生变化。如果已经完成扫描和标记的对象被修改,对新的对象产生了引用,这个新对象就不会被标记,明明是“存活”对象却被回收了。
为了避免这样的问题,和分代回收一样也采用了写屏障。当已经被标记的对象的引用发生变化时,通过写屏障会将新被标记引用的对象作为扫描的起始点记录下来。
由于增量回收的过程时分步渐进试的,可以将中断时间控制在一定时长内。另一方面由于中断操作需要消耗一定时间,GC所消耗的总时长会相应增加。
并行回收的基本原理是:在原油程序运行的同时进行GC操作,这一点和增量回收是相似的。不过相对于在一个CPU上进行GC任务分割的增量回收来说,并行回收可以利用多GPU的性能,尽可能让这些GC任务并行进行。由于软件运行和GC操作是同时进行的,因此就会遇到和增量回收相同的问题。为了解决这个问题,并行回收也需要用写屏障来对当前的状态信息保持更新。不过,让GC操作完全并行,而一点也不影响原有程序的运行,是做不到的。因此在GC操作的某些特定阶段还是需要暂停原有程序的运行。