28

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

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

现实中是如何做到的?

4

4 回答 4

20

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

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

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

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

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

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

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

于 2012-05-22T22:08:41.067 回答
16

Haskell 堆栈在每个堆栈帧中使用单个内存字来描述(使用位图)该堆栈帧中的哪些值是指针,哪些不是。有关详细信息,请参阅GHC 评论中的“堆栈布局”文章和“位图布局”文章。

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

于 2012-05-22T22:04:09.353 回答
11

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

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

于 2012-05-22T22:12:56.180 回答
8

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

于 2012-05-23T07:12:08.053 回答