JVM 垃圾回收算法

2021.01.05 15:35

判断对象是否可回收

引用计数法

这个方法很好理解,简单说就是在对象中添加一个引用计数器,每当有一个地方引用该对象时,引用计数器就加一,每当一个引用失效时,引用计数器就减一,当引用计数器减为 0 时,该对象就是不可能被再次引用的了,需要进行垃圾回收。

这个算法大多数时候都能奏效,而且效率很高,但是这个算法有个很难解决的问题是如何解决对象间的循环依赖。比如对象 objA 和对象 objB 都有字段 instance,当存在 objA.instance=objBobjB.instance=objA 时,尽管没有其他对象再引用这两个对象,但是这两个对象的引用计数器始终都不清零,也就无法进行垃圾回收。因此目前所有的 Java 虚拟机均没有采用引用计数法作为判断对象存活的依据。

可达性分析算法

通过一系列的称为“GC Roots”的根对象作为起始节点集合,从这些节点开始,根据引用关系向下搜索,如果某个对象到 GC Roots 之间没有任何引用链相连,则意味这个对象已经死亡,可以进行垃圾回收。

利用可达性分析算法判定对象是否可回收

在 JVM 中,固定可作为 GC Roots 的对象包括以下几种:

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致 GC 进行时必须“StopTheWorld”的一个重要原因。

垃圾收集算法

当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。 目前在 JVM 中比较常见的三种垃圾收集算法是标记—清除算法(Mark-Sweep)、标记—复制算法(Copying)、标记—整理算法(Mark-Compact)。

标记-清除算法

该算法分为“标记”与“清除”两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收掉所有被标记的对象,也可以反过来标记存活的对象,统一回收未被标记的对象。

标记-清除算法示意图

标记-清除算法主要有两个缺点:

  1. 执行效率不稳定:当堆中包含大量对象,大部分都需要回收的话,那必须进行大量的标记和回收动作,导致标记和清除效率都随对象数量的增长而降低。

  2. 内存碎片化问题:标记清除后会产生大量不连续的内存碎片,可能导致之后的大对象内存分配时明明内存足够却无法找到一片连续的内存空间,不得不提前触发另一次垃圾回收动作。

标记-复制算法

该算法常被简称为“复制算法”。该算法思想是将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在.使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

JVM 经典垃圾收集器中的新生代的 Survivor 0 区和 Survivor 1 区就使用了标记-复制算法。

标记-复制算法示意图

优点:

  1. 实现简单,运行高效。

  2. 回收后的内存连续完整,不存在碎片问题。

缺点:

  1. 需要两倍的内存空间。

  2. 如果系统中的非垃圾对象很多,标记复制算法不会很理想。该算法的高效性建立在存活对象少、垃圾对象多的前提下。

标记-整理算法

在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用标记复制算法,由于存活对象较多,复制的成本也将很高。但是标记清除算法又会产生很多碎片。

执行过程:

标记-整理算法示意图

优点:

缺点:

分代收集算法

前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点,于是有了分代收集算法。

分代收集算法建立在两个分代假说之上:

所以收集器应该将 Java 堆划分为不同的区域,根据对象的年龄(熬过垃圾回收的次数)分配到不同的区域中存储。一般是为了新生代和老年代,其中新生代再进一步细分为 Eden 区、Survivor0 区和 Survivor1 区(有时也叫 frmo 区,to 区)。

默认情况下新生代和老年代的比例是 1:2,也就是说新生代默认占堆的 1/3,该比例可以通过 -XX:NewRatio 进行调整。默认情况下新生代中 Eden 空间和其中一个 Survivor 空间的比例是 8:1,也就是说整体比例是 8:1:1。

堆分代示意图

新生代的特点是区域相对老年代较小,对象生命周期短、存活率低,回收频繁。这种情况使用复制算法,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。

复制算法内存利用率不高的问题,HotSpot 中通过两个 Survivor 区的设计得到缓解:将一个 Survivor 区作为保留空间,每次执行复制算法,是从 Eden 区和非保留空间的 Survivor 区中存活的对象复制到保留空间的 Survivor 区,这样新生代始终只浪费了一个 Survivor 区的内存容量。

老年代的特点是区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。