背景
所有动态创建的对象(在 C++ 和Java使用 new )都在堆中分配内存。如果我们继续创建对象,我们可能会得到 Out Of Memory 错误,因为不可能为对象分配堆内存。因此,我们需要通过为程序不再引用的所有对象(或无法访问的对象)释放内存来清除堆内存,以便为后续的新对象提供可用空间。这段内存可以由程序员自己释放,但这对程序员来说似乎是一种开销,这里垃圾收集来拯救我们,它会自动为所有未引用的对象释放堆内存。
有许多垃圾收集算法在后台运行。其中之一是标记和扫描。
标记和扫描算法
任何垃圾收集算法都必须执行 2 个基本操作。一,它应该能够检测到所有不可达的对象,其次,它必须回收垃圾对象使用的堆空间,并使该空间再次可供程序使用。
上述操作由Mark and Sweep Algorithm分两个阶段进行:
1) 标记阶段
2) 扫描阶段
标记阶段
创建对象时,其标记位设置为 0(假)。在标记阶段,我们将所有可达对象(或用户可以引用的对象)的标记位设置为 1(真)。现在要执行此操作,我们只需要进行图遍历,深度优先搜索方法对我们有用。在这里,我们可以将每个对象视为一个节点,然后访问从该节点(对象)可达的所有节点(对象),直到我们访问了所有可达节点。
- Root 是引用对象的变量,可以通过局部变量直接访问。我们将假设我们只有一个根。
- 我们可以通过以下方式访问对象的标记位:markedBit(obj)。
算法-标记阶段:
Mark(root)
If markedBit(root) = false then
markedBit(root) = true
For each v referenced by root
Mark(v)
注意:如果我们有多个根,那么我们只需为所有根变量调用 Mark()。
扫描阶段
顾名思义,它“清除”无法访问的对象,即清除所有无法访问的对象的堆内存。所有标记值设置为 false 的对象都从堆内存中清除,对于所有其他对象(可达对象),标记位设置为 true。
现在所有可达对象的标记值都设置为 false,因为我们将运行算法(如果需要),我们将再次通过标记阶段来标记所有可达对象。
算法 – 扫描阶段
Sweep()
For each object p in heap
If markedBit(p) = true then
markedBit(p) = false
else
heap.release(p)
mark-and-sweep 算法被称为跟踪垃圾收集器,因为它跟踪整个程序可以直接或间接访问的对象集合。
例子:
a) 所有对象的标记位都设置为 false。
b) 可达对象被标记为真
c) 从堆中清除不可到达的对象。
标记和扫描算法的优点
- 它处理循环引用的情况,即使在循环的情况下,该算法也不会以无限循环结束。
- 在算法的执行过程中不会产生额外的开销。
标记和扫描算法的缺点
- 标记和清除方法的主要缺点是正常程序执行在垃圾收集算法运行时暂停。
- 另一个缺点是,在一个程序上多次运行标记和扫描算法之后,可达对象最终会被许多小的未使用内存区域分隔开。请看下图以更好地理解。
数字:
这里白色块表示空闲内存,而灰色块表示所有可达对象占用的内存。
现在空闲段(用白色表示)大小不一,假设 5 个空闲段的大小为 1、1、2、3、5(单位大小)。
现在我们需要创建一个占用 10 个内存单元的对象,现在假设内存只能以连续的块形式分配,尽管我们有 12 个单元的可用内存空间,但创建对象是不可能的,它会导致内存不足错误。这个问题被称为“碎片化”。我们在“片段”中有可用的内存,但我们无法利用该内存空间。
我们可以通过compaction来减少碎片;我们将内存内容打乱,将所有空闲内存块放在一起,形成一个大块。现在考虑上面的例子,在压缩之后我们有一个连续的 12 个单位的空闲内存块,所以现在我们可以为一个 10 个单位的对象分配内存。
参考:
- Java具有面向对象设计模式的数据结构和算法
- https://en.wikipedia.org/wiki/Tracing_garbage_collection#Na.C3.AFve_mark-and-sweep
- https://blogs.msdn.microsoft.com/abhinaba/2009/01/30/back-to-basics-mark-and-sweep-garbage-collection/