GC算法:基础知识
在深入了解垃圾收集算法的实际实现细节之前,定义所需的术语并理解支持实现的基本原则将是有益的。具体细节因垃圾收集器而异,但一般而言,所有垃圾收集器都集中在以下两个方面:
- 找出所有仍然存活的对象;
- 清除一切所谓的死亡的和未使用的对象。
关于对存活的对象的查找,所有垃圾收集器都是在名为Marking(标记)的过程中实现的。
标记可达对象
现在在JVM中实现的所有GC算法都会从找到所有仍然存活的对象开始垃圾收集工作。使用下面的代表JVM内存布局的图片可以更好地解释这个过程:
首先,GC将一些特定对象定义为 垃圾收集根(Garbage Collection Roots)。这些GC垃圾收集根包括如下对象:
- 当前执行方法的局部变量和输入参数
- 活动线程
- 加载类的静态字段
- JNI引用
接下来,GC遍历内存中的整个对象图,从那些垃圾收集根开始,然后跟随从根到其他对象的引用,例如实例字段等。GC访问到的每个对象都会被标记 为活动对象。
活动对象在上图中显示为蓝色。标记阶段完成后,每个活动对象都会被标记。因此,对于无法从GC根目录访问到的其他所有对象(上图中的灰色数据结构),意味着您的应用程序无法再使用这些无法访问的对象。这些对象被认为是垃圾对象,GC应该在接下来的阶段回收掉它们。
关于标记阶段的需要重点注意的是如下这些点:
- 需要停止应用程序线程以便我们标记对象,因为如果图形始终在不断变化,则无法真正遍历图形。因此需要应用程序线程暂停以便JVM可以完全标记出活动的对象,我们称这个现象为Stop-The-World导致的安全点。可以出于不同原因触发安全点,但垃圾收集是迄今为止引入安全点的最常见原因。
- 此暂停的持续时间既不取决于堆中的对象总数,也不取决于堆的大小,而取决于活动对象的数量 。 因此,增加堆的大小不会直接影响标记阶段的持续时间。
当标记阶段完成后,GC可以继续执行下一步,开始清除不可达的对象。
清除未使用的对象
对于不同的GC算法,清除未使用的对象有些不同,但所有这些GC算法大概可以分为三种:清除,压缩和复制。接下来的部分将更详细地讨论这三种算法。
清除
标记和清除 算法通过忽略这些对象在概念上使用最简单的垃圾方法。这意味着在标记阶段完成之后,未访问对象占用的所有空间都被认为是空闲的,因此可以重新用于分配新对象。
该方法需要用空闲列表记录每个空闲由区域及其大小。空闲列表的管理增加了对象分配的开销。这种算法会存在一个缺点,比如可能当前存在大量的自由区域,但没有单个区域足够大以容纳当前对象的分配,则分配仍将失败( 在Java中会抛出OutOfMemoryError异常)。
压缩
Mark-Sweep-Compact 算法通过将所有标记对象移动到存储区域的开头来解决Mark-Sweep算法的缺点。这种方法的缺点是增加了GC暂停持续时间,因为我们需要将所有对象复制到新位置并更新对这些对象的所有引用。Mark和Sweep的好处也是很明显的 - 在这样的压缩操作之后,通过指针碰撞,新的对象可以很容易分配空间。使用这种方法,空闲空间的位置始终是已知的,并且也不会触发碎片问题。
复制
标记和复制 算法与Mark-Compact非常相似,因为它们也重新定位了所有活动对象。重要的区别在于重新安置的目标是一个不同的内存区域,作为survivors的新地址。标记和复制方法的优点是复制跟标记可以在同一阶段中同时发生。缺点是需要多一个内存区域,并且该区域应足够大以容纳这些存活的对象。