【译文】 GC 安全点 和安全区域

原文链接 : here

 

根引用  Root references
   一个实例死了,意味着它变得无用。只用程序员知道一个实例是否已经无用。为了让程序知道一个实例是否已经无用,我们可以使用编译器分析,引用计数, 或者 可达性分析。

  

  可达性分析假设只要一个实例是可达的,它就是活着的。如果一个实例的引用直接包含在当前函数栈的一个槽(slot)中,它就是直接可达的。那些被可达实例引用的实例也是可达的。因此,可达性分析就是找出那些直接可达的引用,也就是根引用。 根引用的集合就做根集合。

  

  当前执行函数(the mutator)的上下文中 有直接可达的数据,因此 找根集合就是在上下文中找实例的引用。当前函数的上下文引用它的栈和寄存器文件(和一些线程所有的数据)。全局数据也是直接可达的。

 

枚举根集合Root set enumeration
  一般情况下,如果GC使用可达性来确定一个实例是否存活,GC需要获得当前执行现场的一个一致的快照(a consistent snapshot),以便枚举根引用。 这对于stop-the-world 和 并发 GC(多数情况)都适用。 “一致” 意味着快照看起来就像在单个时间点上取得的。 一个一致的根引用快照对于正确性很有必要,否则一些活着的实例就可能丢失。 那现在的问题就是怎样获得当前上下文一致的快照。

  

  为了获得一致的快照,一个简单的办法就是在枚举根引用的过程中,当前执行函数阻塞。 如果在枚举过程中,根集合是不变的,快照也是一致的。

 

  当前执行函数阻塞了它的执行以后,它就不一定能够枚举它上下文中的根引用,除非它在它的上下文中登记并保存了那些引用信息。也就是说,它应该能够分辨出那个栈里有引用,哪个寄存器里有引用。 如果GC能够准确地获得这些信息,它就是准确根集合枚举, 否则就是模糊的。

 

  (对应模糊枚举,GC使用启发式规则来保守地猜测哪些是上下文中的引用。因此,这些GC就是所谓的保守GC。 这篇文章只讨论准确枚举。)

 

  Harmony 通过使用GC安全点和安全区域, 支持准确根集合枚举。

GC安全点和安全区域 Safe-point (or safepoint)
  为了支持准确枚举,JIT编译器需要做一些额外的工作,因为只有JIT准确地知道栈帧信息和寄存器上下文。 当JIT编译一个方法的时候, 对于每一个指令, 它都保存根引用信息,以防执行阻塞在那个指令上。

 

  但是对每一个指令都保存那些信息太昂贵了。 它需要大量空间保存那些信息。 这是不必要的,因为 只有一小部分指令有机会在实际执行时阻塞。 JIT只需要保存那部分指令的信息就够了-- 他们就把叫做安全点。安全点意味着对应根枚举来说,在该点阻塞是安全的。

 

  顺便说一下,并不是所有编程语言的编译器都能够确切知道堆栈信息。 只有安全语言有这个能力。 比如, C/C++就没有。

函数阻塞 Mutator suspension
  对于安全点,现在的问题是,我们怎么保证函数在安全点阻塞。

  有两种基本方法来阻塞当前函数,抢先或者自愿。抢先式的方法是无论任何时候GC需要进行一次收集,它都立即阻塞当前函数的执行。当它发现函数被阻塞在一个不安全的点时,它会恢复函数执行,向前滚动到一个安全点。这种方法在ORP【1】中实现,它是Harmony的前一个版本。但是,目前几乎没有JVM使用这种方法。

 

  在Harmony中实现的方法是自愿阻塞。当GC触发一次垃圾回收,它只是简单的设置一个标志; 当前执行函数会轮询这个标志, 当发现这个标志置位的时候,他们就会阻塞。那些轮询的点都是安全点。 多数情况下, JIT负责在合适的位置插入轮询程序。 有时,VM也需要有一些轮询点。

轮询点 Polling point
  那么哪里是正确轮询GC触发事件的地点呢? 就像我们上面讨论的,我们不想在每一个指令处都设置轮询点。 对于自愿阻塞,一个更严重的问题是轮询负载。因此插入轮询点需要遵循一些基本原则:第一,轮询点应该足够频繁,以便GC不需要等待当前函数阻塞的时间太长, 因为其他函数还在等着GC释放空间来继续执行。第二,轮询点不能太多,导致增加运行负载过重。

 

  最好的结果是刚好有足够的轮询点满足需求:

  1. 一类强制的轮询点是内存分配点。分配可以触发一次垃圾收集,因此分配必须是安全点
  2. 长时间的执行总是和方法调用或者循环 有关。因此,调用点和循环回边点也是期望的轮询点  

 

  这些就是Harmony中的轮询点: 分配点,调用点和循环回边点。 多数情况下运行时负载小于1%。 不幸的是,我们发现单独安全点是不够的。

安全区域 Safe-region
  为什么不够呢? 因为我们忘掉了一种常见执行的情况。我们忘了它,因为它实际上不是长时间执行,而是长时间闲置。有这样一类情况程序不能及时响应GC触发事件,比如sleep,因系统调用阻塞。 这些操作不是JVM能够控制的。JVM在此期间不能够响应GC事件。 因此,我们引入了安全区域的概念来解决这个问题。

 

  安全区域是其中引用不会改变的一段代码片段,那么在其中任一点进行根枚举都是安全的。 换句话说,安全区域是安全点的一个很大的扩展。

 

  在安全点的设计中,如果GC触发事件发生了,执行函数通过轮询进行响应。它通过设置一个准备好的标志(ready flag)来响应。 那么GC就可以进行根枚举了。这是一个握手协议。

 

  安全区域也遵循这个协议。执行函数在进入安全区域时设置ready flag。在它离开安全区域以前,它先检查GC是否完成了枚举(或者收集),并且不再需要执行函数呆在阻塞状态。如果是真的,它就向前执行,离开安全区域; 否则,它就像安全点一样阻塞他自己。

 

  在Harmony的实现中,我们插入 suspend_enable 和 suspend_disable来标志安全区域的界限。



【1】 ORP (Open Runtime Platform), http://orp.sf.net ;

posted @ 2014-04-04 23:02  ridox  阅读(3568)  评论(0编辑  收藏  举报