一、回收算法概述
堆空间是垃圾回收的主要职责之一(注:内存如何分配也涉及,即内存分配与回收),目前垃圾回收算法主要有两类:
·引用计数法:在堆内存中分配对象时,会为对象分配一段额外的空间,用于维护一个计数器,如果对象增加了一个新的引用,则将增加计数器。如果一个引用关系失效则减少计数器。当一个对象的计数器变为0,则说明该对象已经被废弃,可以被回收了。引用计数法需要解决循环依赖的问题,Python语言里,垃圾回收就使用了引用计数法(注:循环依赖使用使用了“标记-清除”(mark and sweep)算法来处理)
·可达性分析法(根引用分析法),基本思路就是将根集合作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象没有被任何引用链访问到时,则证明此对象是不活跃的,可以被回收。
这两种算法各有优缺点,具体可以参考其他文献。JVM的垃圾回收采用了可达性分析法。
垃圾回收算法也一直不断地演化,主要有以下分类:
·垃圾回收算法实现主要分为复制(Copy)、标记清除(Mark-Sweep)和标记压缩(Mark-Compact)。
·在回收方法上又可以分为串行回收、并行回收、并发回收。
·在内存管理上可以分为代管理和非代管理。
分代管理算法
分代管理就是把内存划分成不同的区域进行管理,其思想来源是:有些对象存活的时间短,有些对象存活的时间长,把存活时间短的对象放在一个区域管理,把存活时间长的对象放在另一个区域管理。那么可以为两个不同的区域选择不同的算法,加快垃圾回收的效率。我们假定内存被划分成2个代:新生代和老生代。把容易死亡的对象放在新生代,通常采用复制算法回收;把预期存活时间较长的对象放在老生代,通常采用标记清除算法。
注:分而治之
复制算法
复制算法的实现也有很多种,可以使用两个分区,也可以使用多个分区。使用两个分区时内存的利用率只有50%;使用多个分区(如3个分区),则可以提高内存的使用率。我们这里演示把堆空间分为1个新生代(分为3个分区:Eden、Survivor0、Survivor1)、1个老生代的收集过程。
普通对象创建的时候都是放在Eden区,S0和S1分别是两个存活区。第一次垃圾收集前S0和S1都为空,在垃圾收集后,Eden和S0里面的活跃对象(即可以通过根集合到达的对象)都放入了S1区,如图1-1所示。
图1-1 复制算法第一次回收
回收后Mutator继续运行并产生垃圾,在第二次运行前Eden和S1都有活跃对象,在垃圾收集后,Eden和S1里面的活跃对象(即可以通过根节点到达的对象)都被放入到S0区,一直这样循环收集,如图1-2所示。
图1-2 复制算法第二次回收
标记清除
从根集合出发,遍历对象,把活跃对象入栈,并依次处理。处理方式可以是广度优先搜索也可以是深度优先搜索(通常使用深度优先搜索,节约内存)。标记出活跃对象之后,就可以把不活跃对象清除。下面演示一个简单的例子,从根集合出发查找堆空间的活跃对象,如图1-3所示。
图1-3 标记清除算法
标记清除算法最大的缺点就是使内存碎片化。
标记压缩
标记压缩算法是为了解决标记清除算法中使内存碎片化的问题,除了上述的标记动作之外,还会把活跃对象重新整理从头开始排列,减少内存碎片。
表1-1 垃圾回收基础算法的优缺点
二、 JVM垃圾回收器概述
为了达到最大性能,基于分代管理和回收算法,结合回收的时机,JVM实现的垃圾回收器有:串行回收、并行回收、并发标记回收(CMS)和垃圾优先回收。
串行回收
串行回收使用单线程进行垃圾回收,在回收的时候Mutator需要STW。新生代通常采用复制算法,老生代通常采用标记压缩算法。
并行回收
并行回收使用多线程进行垃圾回收,在回收的时候Mutator需要暂停,新生代通常采用复制算法,老生代通常采用标记压缩算法。
并发标记回收
并发标记回收(CMS)的整个回收期间划分成多个阶段:初始标记、并发标记、重新标记、并发清除等。在初始标记和重新标记阶段需要暂停Mutator,在并发标记和并发清除期间可以和Mutator并发运行。这个算法通常适用于老生代,新生代可以采用并行回收。
垃圾优先回收
垃圾优先回收器(Garbage-First,也称为G1)从JDK7 Update 4开始正式提供。G1致力于在多CPU和大内存服务器上对垃圾回收提供软实时目标和高吞吐量。
连续的内存将导致垃圾回收时收集时间过长,停顿时间不可控。因此G1将堆拆成一系列的分区(Heap Region),这样在一个时间段内,大部分的垃圾收集操作只针对一部分分区,而不是整个堆或整个(老生)代。
连续空间管理
分区空间管理
在G1里,新生代就是一系列的内存分区,这意味着不用再要求新生代是一个连续的内存块。类似地,老生代也是由一系列的分区组成。这样也就不需要在JVM运行时考虑哪些分区是老生代,哪些是新生代。事实上,G1通常的运行状态是:映射G1分区的虚拟内存随着时间的推移在不同的代之间切换。
G1新生代的收集方式是并行收集,采用复制算法。与其他JVM垃圾回收器一样,一旦发生一次新生代回收,整个新生代都会被回收,这也就是我们常说的新生代回收(Young GC)。但是G1和其他垃圾回收器不同的地方在于:
·G1会根据预测时间动态改变新生代的大小。
其他垃圾回收新生代的大小也可以动态变化,但这个变化主要是根据内存的使用情况进行的。G1中则是以预测时间为导向,根据内存的使用情况调整新生代分区的数目。
·G1老生代的垃圾回收方式与其他JVM垃圾回收器对老生代处理有着极大的不同。G1老生代的收集不会为了释放老生代的空间对整个老生代做回收。相反,在任意时刻只有一部分老生代分区会被回收,并且,这部分老生代分区将在下一次增量回收时与所有的新生代分区一起被收集。这就是我们所说的混合回收(Mixed GC)。在选择老生代分区的时候,优先考虑垃圾多的分区,这也正是垃圾优先这个名字的由来。
在G1中还有一个概念就是大对象,指的是待分配的对象大小超过一定的阈值之后,为了减少这种对象在垃圾回收过程的复制时间,直接把对象分配到老生代分区中而不是新生代分区中。
从实现角度来看,G1算法是复合算法,吸收了以下算法的优势:
·列车算法,对内存进行分区。
·CMS,对分区进行并发标记。