2

我正在观看视频Google IO 2008 - Dalvik Virtual Machine Internals以了解 Dalvik VM 的工作原理以及为什么这些人更喜欢 Dalvik VM 而不是 JVM for android。我发现 android 使用单独的内存来存储有关对象的垃圾信息,这与我们将标记位(告诉对象是否能够进行垃圾收集的位)与对象一起使用的 JVM 不同。

谁能详细告诉我为标记位设置单独的内存而没有为标记位设置单独的内存的优缺点是什么?

我无法通过观看视频来获得这种差异。

4

2 回答 2

3

单独位图的一些优点:

  • 密集得多。典型的 GC 可能需要 8 位 GC 元数据,但由于对齐,对象内标头可能会将此内存四舍五入到 32 位。
  • 一些操作,尤其是围绕扫描的操作,变得更快。这部分是因为更密集(见上文)的位图意味着更少的内存流量和更好的缓存使用,还因为某些操作(例如,将所有标记位归零)可以在这种格式下进行矢量化。(需要设计 GC 的其他部分以利用该功能。)
  • 如果您fork()在 Unix 系统上,单独的位标记可以更好地利用写时复制:包含对象的页面可能保持共享。

对象内标记位的一些优点:

  • 根据用于将对象与位图关联的方案,获取对象的标记位(反之亦然)可能非常复杂和/或缓慢。另一方面,对象内标头易于访问。
  • 更轻松的内存管理:无需创建正确大小的单独分配并保持同步。
  • 许多用于查找对象位图的快速方案(反之亦然)在其他方面具有很大的限制性。例如,如果您为每个页面创建一个位图并将位图指针存储在页面的开头,那么您在存储大于页面的对象时就会遇到问题。
于 2014-04-14T20:07:23.630 回答
1

单独的标记位通过一个位数组来工作,其中每个位代表堆中可以启动对象的地址。例如,假设堆是 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 显然非常快。因此,使用单独的标记位,扫描阶段可能会更快。

但是还有一个皱纹!在任何标记和扫描收集器中,运行时间主要由标记阶段决定,而不是通常非常快的扫描阶段。所以判决还没有出来。有些人更喜欢单独的标记位,其他人更喜欢对象内的标记位。据我所知,目前还没有人能够证明哪种方法优于另一种方法。

于 2016-12-09T18:02:29.703 回答