在具有Haskell或Go等自动垃圾收集的语言中,垃圾收集器如何找出存储在堆栈中的哪些值是指向内存的指针以及哪些只是数字?如果垃圾收集器只扫描堆栈并假定所有地址都是对象的引用,那么很多对象可能会被错误地标记为可达。
显然,可以在每个堆栈框架的顶部添加一个值,该值描述了下一个值中有多少是指针,但这不会花费很多性能吗?
现实情况如何?
答案 0 :(得分:20)
一些收藏家认为堆栈上的所有东西都是潜在的指针(如Boehm GC)。事实证明并没有人们想象的那么糟糕,但显然不是最理想的。更常见的是,在托管语言中,堆栈会留下一些额外的标记信息,以帮助收集器找出指针所在的位置。
请记住,在大多数编译语言中,每次输入函数时堆栈框架的布局都是相同的,因此确保以正确的方式标记数据并不困难。
“位图”方法是这样做的一种方法。位图的每个位对应于堆栈上的一个字。如果该位是1,那么堆栈上的位置是指针,如果它是0,那么该位置只是从收集器的角度来看的数字(或沿着这些线的某些东西)。写得非常好的GHC运行时和调用约定对大多数函数使用单字布局,这样一些位传达堆栈帧的大小,其余用作位图。较大的堆栈帧需要多字结构,但想法是一样的。
关键是开销很低,因为布局信息是在编译时计算的,然后每次调用函数时都包含在堆栈中。
更简单的方法是“指针优先”,其中所有指针都位于堆栈的开头。您只需要在指针之前包含一个长度,或者在它们之后包含一个特殊的“结束”单词,以告诉哪些单词是给定此布局的指针。
有趣的是,尝试将此管理信息传递到堆栈会产生许多与C语言互操作相关的问题。例如,将高级语言编译为C是次优的,因为即使C是可移植的,它也是很难携带这种信息。优化为类C语言(GCC,LLVM)设计的编译器可能会重构堆栈帧,从而产生问题,因此GHC LLVM后端使用自己的“堆栈”而不是LLVM堆栈,这使得它需要进行一些优化。同样,需要仔细构建C代码和“托管”代码之间的边界,以免混淆GC。
因此,当您在JVM上创建新线程时,实际上创建了两个堆栈(一个用于Java,一个用于C)。
答案 1 :(得分:16)
Haskell堆栈在每个堆栈帧中使用单个内存字来描述(使用位图)该堆栈帧中的哪些值是指针而哪些不是。有关详细信息,请参阅GHC评论中的"Layout of the stack"文章和"Bitmap layout"文章。
公平地说,所有事情都考虑到,单个记忆单词的成本并不高。你可以把它想象成只为每个方法添加一个变量;这并不是那么糟糕。
答案 2 :(得分:11)
存在GC假设每个位模式都是GC管理的事物的地址实际上是一个指针(因此不释放某些东西)。这实际上可以很好地工作,因为调用指针通常比小的公共整数大,并且通常必须对齐。但是,是的,这可能导致某些对象的收集被延迟。 C语言的Boehm收集器以这种方式工作,因为它是基于库的,所以没有得到编译器的任何特定帮助。
还有一些GC与他们使用的语言紧密耦合,实际上知道内存中对象的结构。我从来没有专门读过堆栈帧处理,但如果编译器和GC设计为一起工作,你可以记录信息以帮助GC。一个技巧是将所有指针引用放在一起,并使用每个堆栈帧一个字来记录有多少,这不是一个巨大的开销。如果你可以找出与每个堆栈框架相对应的函数而不添加一个单词,那么你可以编译一个按功能的“堆栈框架布局图”。另一种选择是使用标记的单词,你设置低命令不是指向1的字的位,指针永远不需要(由于地址对齐),所以你可以区分它们。这意味着您必须移动未装箱的值才能使用它们。
答案 3 :(得分:8)
重要的是要意识到GHC维护自己的堆栈并且不使用C堆栈(除了FFI调用之外)。没有可移植的方法来访问C堆栈的所有内容(例如,在SPARC中,其中一些内容隐藏在寄存器窗口中),因此GHC维护一个完全控制的堆栈。一旦你维护自己的堆栈,你可以选择任何方案来区分堆栈上的非指针(如使用位图)。