【问题标题】:How can a garbage collector find out about object references done from the stack?垃圾收集器如何找出堆栈中的对象引用?
【发布时间】:2012-05-29 10:48:03
【问题描述】:

在 Haskell 或 Go 等具有自动垃圾收集功能的语言中,垃圾收集器如何找出堆栈中存储的哪些值是指向内存的指针,哪些只是数字?如果垃圾收集器只是扫描堆栈并假定所有地址都是对对象的引用,那么很多对象可能会被错误地标记为可达。

显然,可以在每个堆栈帧的顶部添加一个值,用于描述有多少下一个值是指针,但这不会花费很多性能吗?

现实中是怎么做到的?

【问题讨论】:

    标签: haskell garbage-collection go


    【解决方案1】:

    Haskell 堆栈在每个堆栈帧中使用一个内存字来描述(使用位图)该堆栈帧中的哪些值是指针,哪些不是。有关详细信息,请参阅 GHC 评论中的"Layout of the stack" 文章和"Bitmap layout" 文章。

    说句公道话,一个单词的记忆真的没有太多成本,考虑到所有的事情。您可以将其视为只是为每个方法添加一个变量;这还不算太糟糕。

    【讨论】:

    • 小修正 - 单个单词在堆栈帧信息表中,而不是堆栈帧本身。或者换一种说法:堆栈帧只包含返回指针,可以通过它找到代码以及编码堆栈布局的信息表。这意味着堆栈上没有实际的空间开销。
    • 嗯,好吧。我被以下段落误导了:“与主要具有“指针优先”布局的堆对象不同,在堆栈帧中,指针和非指针混合在一起......因此堆栈帧具有位图布局。”所以如果我理解正确的话,位图布局意味着在某个地方有一个位图——但不是在堆栈本身上——带有这些信息。我想对同一方法的调用将始终具有相同的堆栈框架布局,因此位图在信息表中被共享?
    • 请注意,我们在这里讨论的不是调用,而是返回闭包。但是是的,两者都静态地知道他们框架的堆栈布局,因为毕竟数据在堆栈上的唯一目的是为了他们使用它。
    【解决方案2】:

    一些收集器假设堆栈上的所有内容都是潜在指针(如 Boehm GC)。事实证明,这并不像人们预期的那么糟糕,但显然不是最理想的。更常见的是,在托管语言中,一些额外的标记信息会留在堆栈中,以帮助收集器找出指针的位置。

    请记住,在大多数编译语言中,每次输入函数时堆栈帧的布局都是相同的,因此确保以正确的方式标记数据并不难。

    “位图”方法是执行此操作的一种方式。位图的每一位对应于堆栈上的一个字。如果该位是 1,则堆栈上的位置是一个指针,如果它是 0,那么从收集器的角度来看,该位置只是一个数字(或类似的东西)。编写得非常出色的 GHC 运行时和调用约定对大多数函数使用单字布局,这样一些位会传达堆栈帧的大小,其余位用作位图。较大的栈帧需要多字结构,但思路是一样的。

    关键是开销很低,因为布局信息是在编译时计算的,然后在每次调用函数时都包含在堆栈中。

    更简单的方法是“指针优先”,所有指针都位于堆栈的开头。您只需要在指针之前包含一个长度,或者在它们之后包含一个特殊的“结束”字,以告诉哪些字是给定此布局的指针。

    有趣的是,试图将这些管理信息放到堆栈上会产生一系列与 C 互操作相关的问题。例如,将高级语言编译为 C 是次优的,因为即使 C 是可移植的,它也是很难携带这种信息。优化为类 C 语言(GCC、LLVM)设计的编译器可能会重组堆栈框架,从而产生问题,因此 GHC LLVM 后端使用自己的“堆栈”而不是 LLVM 堆栈,这会导致一些优化。同样,需要仔细构建 C 代码和“托管”代码之间的边界,以免混淆 GC。

    因此,当您在 JVM 上创建新线程时,实际上会创建两个堆栈(一个用于 Java,一个用于 C)。

    【讨论】:

    • 对于某些平台(如IA-64、x86-64)和操作系统(如Linux),可以使用libunwind(nongnu.org/libunwind)解开栈并访问C栈帧。这消除了处理 C 堆栈的一些复杂性(但激进的编译器优化仍可能导致问题)。
    • 为什么 Haskell 使用位图方法?使用指针优先的方法不是更好吗?
    • 看来LLVM确实有support for garbage collection
    • @FUZxxl,来自 GHC 评论:“与主要具有“指针优先”布局的堆对象不同,在堆栈帧中,指针和非指针混合在一起。这样我们就可以支持“ stack stubbing”,即存储在堆栈中的活动变量稍后可以通过推送一个新的堆栈帧来标记为死变量,该堆栈帧将该插槽标识为包含非指针,因此 GC 不会跟随它。”
    【解决方案3】:

    存在 GC 假设每个位模式,即 GC 管理的某物的地址 实际上是一个指针(因此不要释放某物)。这实际上可以很好地工作,因为调用指针通常大于小的常见整数,并且通常必须对齐。但是,是的,这可能会导致某些对象的收集被延迟。 C 的 Boehm 收集器以这种方式工作,因为它是基于库的,因此不需要从编译器获得任何具体帮助。

    还有一些 GC 与它们所使用的语言更紧密地耦合,并且实际上知道内存中对象的结构。我从未专门阅读过堆栈帧处理方面的内容,但如果编译器和 GC 旨在协同工作,您可以记录信息以帮助 GC。一个技巧是将所有指针引用放在一起,并使用每个堆栈帧一个字来记录有多少个,这并不是一个巨大的开销。如果您可以在不添加单词的情况下计算出每个堆栈帧对应的函数,那么您可以编译每个函数的“堆栈帧布局图”。另一种选择是使用标记词,您可以在其中设置低不是指向 1 的指针的单词的顺序位,指针永远不需要(由于地址对齐),因此您可以将它们区分开来。这意味着您必须移动未装箱的值才能使用它们。

    【讨论】:

    • +1 用于讨论 Boehm 收集器,它使用了一种非常出色的方法。
    【解决方案4】:

    重要的是要认识到 GHC 维护自己的堆栈并且不使用 C 堆栈(FFI 调用除外)。没有可移植的方式来访问 C 堆栈的所有内容(例如,在 SPARC 中,其中一些内容隐藏在寄存器窗口中),因此 GHC 维护一个堆栈,它可以完全控制它。维护自己的堆栈后,您可以选择任何方案来区分堆栈上的指针和非指针(例如使用位图)。

    【讨论】:

      猜你喜欢
      • 2013-12-30
      • 2012-06-03
      • 2023-03-17
      • 1970-01-01
      • 2011-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-11
      相关资源
      最近更新 更多