单独的标记位通过一个位数组来工作,其中每个位代表堆中可以启动对象的地址。例如,假设堆是 65536 字节并且所有对象都在 16 字节边界对齐,那么堆中有 4096 个地址可以作为对象的开始。这意味着数组需要包含 4096 位,可以有效地存储为 512 字节或 64 个 64 位大小的无符号整数。
对象内标记位的工作原理是,如果对象被标记,则将每个对象的每个标头的一位设置为 1,否则设置为 0。请注意,这要求每个对象都有一个专用的标题区域。JVM 和 .NET 等运行时都会向对象添加标头,因此您基本上可以免费获得标记位的空间。
但它不适用于无法完全控制其运行环境的保守收集器,例如Boehm GC。他们可能会将整数误认为是指针,因此对他们来说修改 mutators 数据堆中的任何内容都是有风险的。
Mark & sweep 垃圾回收分为两个阶段:标记和清扫。使用对象内标记位进行标记是直截了当的(伪代码):
if not obj.is_marked():
obj.mark()
mark_stack.append(obj)
使用单独的数组来存储标记位,我们必须将对象的地址和大小转换为位数组中的索引,并将相应的位设置为 1:
obj_bits = obj.size_in_bytes() / 16
bit_idx = (obj - heap.start_address()) / 16
if not bitarr.bit_set(bit_idx):
bitarr.set_range(bit_idx, obj_bits)
mark_stack.append(obj)
因此,在我们的示例中,如果一个对象的长度为 128 字节,则将在位数组中设置 8 位。显然,使用对象内标记位要简单得多。
但是单独的标记位在扫描时会获得一些动力。扫描涉及扫描整个堆并找到未标记的连续内存区域,因此可以回收。使用对象内标记位,大致如下所示:
iter = heap.start_address()
while iter < heap.end_address():
# Scan til the next unmarked object
while iter.is_marked():
iter.unmark()
iter += iter.size()
if iter == heap.end_address():
return
# At an unmarked block
start = iter
# Scan til the next marked object
while iter < heap.end_address() and not iter.is_marked():
iter += iter.size()
size = iter - start
# Reclaim the block
heap.reclaim(start, size)
注意迭代如何在行中从一个对象跳到另一个对象iter += iter.size()
。这意味着扫描阶段的运行时间与活动对象和垃圾对象的总数成正比。
使用单独的标记位,您将执行大致相同的循环,除了大片垃圾对象会飞过而不会“停止”每个对象。
再次考虑 65536 堆。假设它包含 4096 个都是垃圾的对象。迭代标记位数组中的 64 个 64 位整数并看到它们都是 0 显然非常快。因此,使用单独的标记位,扫描阶段可能会更快。
但是还有一个皱纹!在任何标记和扫描收集器中,运行时间主要由标记阶段决定,而不是通常非常快的扫描阶段。所以判决还没有出来。有些人更喜欢单独的标记位,其他人更喜欢对象内的标记位。据我所知,目前还没有人能够证明哪种方法优于另一种方法。