jvm系列算法
GC的基本算法,大致上都逃不出标记清除法/标记压缩法、复制收集算法、引用计数法这三种方式以及它们的衍生品。如今,经过对这三种方式进行融合,出现了一些更加高级的方式。这里,咱们介绍一下其中最有表明性的三种,即分代回收、增量回收和并行回收。有些状况下,也能够对这些方法中的几种进行组合使用。
首先,咱们来说讲高级GC技术中最重要的一种,即分代回收(Generational GC)。因为GC和程序处理的本质是无关的,所以它所消耗的时间越短越好。分代回收的目的,正是为了在程序运行期间,将GC所消耗的时间尽可能缩短。分代回收的基本思路,是利用了通常性程序所具有的性质,即大部分对象都会在短期内成为垃圾,而通过必定时间依然存活的对象每每拥有较长的寿命。若是寿命长的对象更容易存活下来,寿命短的对象则会被很快废弃,那么到底怎样作才能让GC变得更加高效呢?若是对分配不久,诞生时间较短的“年轻”对象进行重点扫描,应该就能够更有效地回收大部分垃圾。
在分代回收中,对象按照生成时间进行分代,刚刚生成不久的年轻对象划为新生代(Young gen-eration),而存活了较长时间的对象划为老生代(Old generation)。根据具体实现方式的不一样,可能还会划分更多的代,在这里为了讲解方便,咱们就先限定为两代。若是上述关于对象寿命的假说成立的话,那么只要仅仅扫描新生代对象,就能够回收掉废弃对象中的很大一部分。像这种只扫描新生代对象的回收操做,被称为小回收(Minor GC)。小回收的具体回收步骤以下。首先从根开始一次常规扫描,找到“存活”对象。这个步骤采用标记清除或者是复制收集算法均可以,不过大多数分代回收的实现都采用了复制收集算法。须要注意的是,在扫描的过程当中,若是遇到属于老生代的对象,则不对该对象继续进行递归扫描。这样一来,须要扫描的对象数量就会大幅减小。而后,将第一次扫描后残留下来的对象划分到老生代。具体来讲,若是是用复制收集算法的话,只要将复制目标空间设置为老生代就能够了;而用标记清除算法的话,则大多采用在对象上设置某种标志的方式。
从任何地方都没有进行引用的老生代中的F对象,会经过大回收操做进行回收。
对来自老生代的引用进行记录这个时候,问题出现了,从老生代对象对新生代对象的引用怎么办呢?若是只扫描新生代区域的话,那么从老生代对新生代的引用就不会被检测到。这样一来,若是一个年轻的对象只有来自老生代对象的引用,就会被误认为已经“死亡”了。所以,在分代回收中,会对对象的更新进行监视,将从老生代对新生代的引用,记录在一个叫作记录集(remembered set)的表中(图5)。在执行小回收的过程当中,这个记录集也做为一个根来对待。
要让分代回收正确工做,必须使记录集的内容保持更新。为此,在老生代到新生代的引用产生的瞬间,就必须对该引用进行记录,而负责执行这个操做的子程序,须要被嵌入到全部涉及对象更新操做的地方。这个负责记录引用的子程序是这样工做的。设有两个对象:A和B,当对A的内容进行改写,并加入对B的引用时,若是①A属于老生代对象,②B属于新生代对象,则将该引用添加到记录集中。这种检查程序须要对全部涉及修改对象内容的地方进行保护,所以被称为写屏障(Write barrier)。写屏障不只用于分代回收,同时也用在不少其余的GC算法中。虽然说老生代区域中的对象通常来讲寿命都比较长,但也决不是“不老不死”的。随着程序的运行,老生代区域中的“死亡”对象也在不断增长。为了不这些死亡的老生代对象白白占用内存空间,偶尔须要对包括老生代区域在内的所有区域进行一次扫描回收。像这样以所有区域为对象的GC操做被称为彻底回收(Full GC)或者大回收(Major GC)。分代回收经过减小GC中扫描的对象数量,达到缩短GC带来的平均中断时间的效果。不过因为仍是须要进行大回收,所以最大中断时间并无获得什么改善。从吞吐量来看,在对象寿命假说可以成立的程序中,因为扫描对象数量的减小,能够达到很是不错的成绩。可是,其性能会被程序行为、分代数量、大回收触发条件等因素大幅度左右。
在对实时性要求很高的程序中,比起缩短GC的平均中断时间,每每更重视缩短GC的最大中断时间。例如,在机器人的姿式控制程序中,若是由于GC而让控制程序中断了0.1秒,机器人可能就摔倒了。或者,若是车辆制动控制程序由于GC而延迟响应的话,后果也是不堪设想的。在这些对实时性要求很高的程序中,必须可以对GC所产生的中断时间作出预测。例如,能够将“最多只能中断10毫秒”做为附加条件。在通常的GC算法中,做出这样的保证是不可能的,由于GC产生的中断时间与对象的数量和状态有关。
所以,为了维持程序的实时性,不等到GC所有完成,而是将GC操做细分红多个部分逐一执行。这种方式被称为增量回收(Incremental GC)。在增量回收中,因为GC过程是渐进的,在回收过程当中程序自己会继续运行,对象之间的引用关系也可能会发生变化。若是已经完成扫描和标记的对象被修改,对新的对象产生了引用,这个新对象就不会被标记,明明是“存活”对象却被回收掉了。在增量回收中为了不这样的问题,和分代回收同样也采用了写屏障。当已经被标记的对象的引用关系发生变化时,经过写屏障会将新被引用的对象做为扫描的起始点记录下来。因为增量回收的过程是分步渐进式的,能够将中断时间控制在必定长度以内。另外一方面,因为中断操做须要消耗必定的时间,GC所消耗的总时间就会相应增长,正所谓有得必有失。
最近的计算机中,一块芯片上搭载多个CPU核心的多核处理器已经逐渐普及。不只是服务器,就连我的桌面电脑中,多核CPU也已经成了屡见不鲜。例如美国英特尔公司的Core i7就拥有6核12个线程。在这样的环境中,就须要经过利用多线程来充分发挥多CPU的性能。并行回收正是经过最大限度利用多CPU的处理能力来进行GC操做的一种方式。并行回收的基本原理是,是在原有的程序运行的同时进行GC操做,这一点和增量回收是类似的。不过,相对于在一个CPU上进行GC任务分割的增量回收来讲,并行回收能够利用多CPU的性能,尽量让这些GC任务并行(同时)进行。因为软件运行和GC操做是同时进行的,所以就会遇到和增量回收相同的问题。为了解决这个问题,并行回收也须要用写屏障来对当前的状态信息保持更新。不过,让GC操做彻底并行,而一点都不影响原有程序的运行,是作不到的。所以在GC操做的某些特定阶段,仍是须要暂停原有程序的运行。在多核化快速发展的如今,并行回收也成了一个很是重要的话题,它的算法也在不断进行改善。在硬件系统的支持下,无需中断原有程序的彻底并行回收器也已经呼之欲出。从此,这个领域至关值得期待。