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

时间:2015-11-25 23:55:36

标签: assembly compiler-construction 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)
}

调用时栏的堆栈框架

bar's stackframe during invocation

在这个例子中,bar的堆栈帧有一个局部变量x,它是指向堆上值的指针,其中参数y和z是直接整数值。

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

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

3 个答案:

答案 0 :(得分:10)

  

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

有几种可能的方法

  • 保守的堆栈扫描。一切都被视为潜在的指针。这导致GC不精确。不精确的扫描会阻止对象重新定位,这反过来会阻止semi-space/compacting GCs的实施或使其复杂化。
  • 如上所述标记位。这可以被认为是稍微不那么保守的扫描,但它仍然是不精确的
  • 编译器在任何给定时间保留对精确堆栈布局的知识,即指针所在的位置。由于这可以从指令变为指令而指针也可以驻留在寄存器中,这将是非常复杂的。
    作为简化,它仅针对特定点进行,在这些点上所有线程可以通过已知的方式协同地将控制权交给GC。另一个线程请求GC时的堆栈布局。这称为安全点(如下所述)。
  • 其他机制可能是可能的,例如将堆栈划分为引用和非引用条目,并始终确保已注册的引用也位于堆栈的某个位置,但我不知道该方法的实用性

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

  

以下是关于"什么是安全点"的陈述的集合。那   尝试既正确又有点精确:

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

    [...]

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

      [...]

答案 1 :(得分:5)

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

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

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

其他一些观点:

  • 所有类型的GC都存在识别参考文献的问题。

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

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

答案 2 :(得分:1)

我发现了另一种可能的方法,描述为Emery's Idea

  
      
  • 运行程序的两个副本。检查可疑指针时检查两个内存副本。
  •   
  • 如果有问题的int /指针在两个程序中都相同,那么它就是一个int。
  •   
  • 如果int /指针具有相同的基数但是具有不同的偏移量,则它是指针。
  •   

我可以看到这在实际示例中具有显着的性能开销,但对于顺序语言或者使用缩减计时器方法在用户空间中同时在单个核心上运行的语言可能是可能的。

相关问题