内存引用如何位于移动垃圾收集实现中?

Jak*_*ake 9 compiler-construction assembly garbage-collection

在移动的垃圾收集器中,必须有一种精确的方法来区分堆栈和堆上的哪些值是引用,哪些是立即值.这个细节似乎在我读过的关于垃圾收集的大部分文献中都被掩盖了.

我已经研究过为每个堆栈帧分配一些前导码是否有效,例如,在调用之前描述每个参数.但肯定所有这一切都将问题推向了间接的上层.然后,当在GC循环期间遍历它以获得立即值或引用时,如何区分前导码和堆栈帧?

有人可以解释一下这是如何在现实世界中实现的吗?

下面是这个问题的示例程序,它使用第一个类函数词法闭包及其堆栈框架图和位于堆上的父代环境:

一个示例程序

def foo(x) = {
    def bar(y,z) = {
        return x + y + z
    }
    return bar
}


def main() = {
    let makeBar = foo(1)
    makeBar(2,3)
}
Run Code Online (Sandbox Code Playgroud)

调用时Bar的堆栈框架:

调用期间bar的堆栈框架

在此示例中,bar的堆栈帧具有局部变量x,它是指向堆上的值的指针,其中参数y和z是直接整数值.

我读到,Objective CAML对堆栈上的每个值使用一个标记位,该值为每个值添加前缀.允许在GC循环期间对每个值进行二进制ref-or-imm检查.但这可能会产生一些不必要的副作用.整数限制为31位,并且需要调整原始计算的动态代码生成以补偿这一点.简而言之 - 感觉有点太脏了.必须有一个更优雅的解决方案.

是否有可能知道并静态访问此信息?比如以某种方式将类型信息传递给垃圾收集器?

the*_*472 10

有人可以解释一下这是如何在现实世界中实现的吗?

有几种可能的方法

  • 保守的堆栈扫描.一切都被视为潜在的指针.这导致GC不精确.不精确的扫描可防止物体重新定位,从而防止或使半空间/压实GC的实施变得复杂.
  • 如你所提到的那样标记位.这可以被认为是略微不那么保守的扫描,但它仍然是不精确的
  • 编译器在任何给定时间保留对精确堆栈布局的知识,即指针所在的位置.由于这可以从指令变为指令而指针也可以驻留在寄存器中,因此这将非常复杂.
    作为简化,仅针对特定点进行,当另一个线程请求GC时,所有线程可以使用已知的堆栈布局协同地将控制权移交给GC.这称为安全点(如下所述).
  • 其他机制也许是可能的,例如将堆栈划分为引用和非引用条目,并始终确保已注册的引用也位于堆栈的某个位置,但我不知道该方法的实用性如何

Gil Tene有一个很好的,虽然主要是JVM特定的安全点的解释,所以我将在这里引用相关部分:

这是一个关于"什么是安全点"的声明的集合,它试图既正确又精确:

  1. 线程可以处于安全点或不处于安全点.当处于安全点时,线程对其Java机器状态的表示被很好地描述,并且可以被JVM中的其他线程安全地操纵和观察.当不在安全点时,线程对java机器状态的表示将不会被JVM中的其他线程操纵.[注意,其他线程不会操纵线程的实际逻辑机器状态,只是它表示该状态.更改机器状态表示的一个简单示例是更改java引用堆栈变量指向的虚拟地址,作为重定位该对象的结果.引用变量的逻辑状态不受此更改的影响,因为引用仍然引用同一个对象,并且引用同一对象的两个引用变量在逻辑上仍然彼此相等,即使它们临时指向不同的虚拟地址].

[...]

  1. 所有[实用] JVM都应用一些高效的机制来频繁地跨越安全点机会,其中线程实际上不会进入安全点,除非其他人指出需要这样做.例如,生成的代码中的大多数调用站点和循环后备将包括某种类型的安全点轮询序列,相当于"我现在需要转到安全点吗?".许多HotSpot变体(OpenJDK和Oracle JDK)当前使用一个简单的全局"转到安全点"指示器,其形式是在需要安全点时受到保护的页面,否则不受保护.此机制的安全点轮询相当于该页面中固定地址的负载.如果负载陷入SEGV,则线程知道它需要进入安全点.Zing使用了一种效率相似的不同的每线程安全点指示器.

[...]


Ste*_*n C 5

上面的答案确定了三个主要的替代方案.已经尝试了第三种替代品的变体:

  • 让编译器对堆栈和对象框架中的变量进行分区/重新排序,以便(例如)引用变量位于标量变量之前.

这意味着需要在运行时保留的类型信息是单个数字.这可以存储在帧本身中,或者以正常方式存储在与类或方法相关联的类型信息中.但是,这引入了其他开销; 例如,需要双栈和堆栈指针.根据经验,这不是一场胜利.

其他一些观点:

  • 各种GC都存在识别参考文献的问题.

  • 如果你采用"保守"方法,(参考标识可能不准确),那么你就无法安全地压缩堆.这包括各种复制收集器.

  • 标记位(除非它们是硬件支持的)对于有效的算术运算可能是有问题的.(如果你需要"窃取"一点来区分指针和非指针,那么算术运算需要额外的指令来补偿.FWIW,MIT CLU编译器用来做这个......早在1980年代.CLU GC是一个准确的标记/扫描/紧凑收集器,但整数算术很慢......我不记得它们是如何处理浮点的.)