关于OopMap、SafePoint(安全点)以及安全区域

1.OopMap

之前我们提到,在正式的GC之前总是需要进行可达性分析来查找内存中所有存活的对象,以便GC能够正确的回收已经死亡的对象。那么对于一个十分复杂的系统,每次GC的时候都要遍历所有的引用肯定是不现实的。因为在可达性分析的时候,需要进行Stop The World,程序中的线程需要停止来配合可达性分析。就好像是你女朋友在打扫卫生的时候(什么,你还没有女朋友?这还能难道程序员了?new 一个啊!),肯定不会让你走来走去的。所以,你肯定在内心里也希望你女朋友打扫卫生快一点,因为你的膀胱已经快要爆炸了。对于程序来说有也一样,也希望GC的时候快一点,以便让程序高效地完成工作。

所以,每次直接遍历整个引用链肯定是不现实的。 为了应对这种尴尬的问题,最早有保守式GC和后来的准确式GC。这里准确式GC就会提到一个OopMap,用来保存类型的映射表。

  1. 保守式GC

在进行GC的时候,会从一些已知的位置(GC Roots)开始扫描内存,扫描到一个数字就判断他是不是可能是指向GC堆中的一个指针(这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。)。然后一直递归的扫描下去,最后完成可达性分析。这种模糊的判断方法因为无法准确判断一个位置上是否是真的指向GC堆中的指针,所以被命名为保守式GC。这种可达性分析的方式因为不需要准确的判断出一个指针,所以效率快,但是也正因为这种特点,他存在下面两个明显的缺点:

  • 因为是模糊的检查,所以对于一些已经死掉的对象,很可能会被误认为仍有地方引用他们,GC也就自然不会回收他们,从而引起了无用的内存占用,就是典型的占着茅坑不拉屎,造成资源浪费。
  • 由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法可以在使用保守式GC的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。但是这样的话引用的访问速度就降低了。Sun JDK的Classic VM用过这种全handle的设计,但效果实在算不上好。

2.准确式GC

与保守式GC相对的就是准确式GC,何为准确式GC?就是我们准确的知道,某个位置上面是否是指针,对于java来说,就是知道对于某个位置上的数据是什么类型的,这样就可以判断出所有的位置上的数据是不是指向GC堆的引用,包括栈和寄存器里的数据。

网上看了下说是实现这种要求的方法有好几种,但是在java中实现的方式是:从我外部记录下类型信息,存成映射表,在HotSpot中把这种映射表称之为OopMap,不同的虚拟机名称可能不一样。

实现这种功能,需要虚拟机的解释器和JIT编译器支持,由他们来生成OopMap。生成这样的映射表一般有两种方式:

  • 每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”; 
  • 为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。

总而言之,GC开始的时候,就通过OopMap这样的一个映射表知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录下栈和寄存器中哪些位置是引用。

2.SafePoint(安全点)

上面讲到了为了快点进行可达性的分析,使用了一个引用类型的映射表,可以快速的知道对象内或者栈和寄存器中哪些位置是引用了。

但是随着而来的又有一个问题,就是在方法执行的过程中, 可能会导致引用关系发生变化,那么保存的OopMap就要随着变化。如果每次引用关系发生了变化都要去修改OopMap的话,这又是一件成本很高的事情。所以这里就引入了安全点的概念。

什么是安全点?OopMap的作用是为了在GC的时候,快速进行可达性分析,所以OopMap并不需要一发生改变就去更新这个映射表。只要这个更新在GC发生之前就可以了。所以OopMap只需要在预先选定的一些位置上记录变化的OopMap就行了。这些特定的点就是SafePoint(安全点)。由此也可以知道,程序并不是在所有的位置上都可以进行GC的,只有在达到这样的安全点才能暂停下来进行GC。

既然安全点决定了GC的时机,那么安全点的选择就至为重要了。安全点太少,会让GC等待的时间太长,太多会浪费性能。所以安全点的选择是以程序“是否具有让程序长时间执行的特征”为标准的(这句话是从书上看来的,不知道作者自己能不能看明白这话啥意思,反正我是看不懂),所以我们这里了解一下结果就行了。一般会在如下几个位置选择安全点:

  1. 循环的末尾 
  2. 方法临返回前 / 调用方法的call指令后 
  3. 可能抛异常的位置

还有一个需要考虑的问题就是,如何让程序在要进行GC的时候都跑到最近的安全点上停顿下来。这里有两种方案:

  1. 抢断式中断

抢断式中断就是在GC的时候,让所有的线程都中断,如果这些线程中发现中断地方不在安全点上的,就恢复线程,让他们重新跑起来,直到跑到安全点上。(现在几乎没有虚拟机采用这种方式,原因不详)

  1. 主动式中断

主动式中断在GC的时候,不会主动去中断线程,仅仅是设置一个标志,当程序运行到安全点时就去轮训该位置,发现该位置被设置为真时就自己中断挂起。所以轮训标志的地方是和安全点重合的,另外创建对象需要分配内存的地方也需要轮询该位置。

3.安全区域

安全点的使用似乎解决了OopMap计算的效率的问题,但是这里还有一个问题。安全点需要程序自己跑过去,那么对于那些已经停在路边休息或者看风景的程序(比如那些处在Sleep或者Blocked状态的线程),他们可能并不会在很短的时间内跑到安全点去。所以这里为了解决这个问题,又引入了安全区域的概念。

安全区域很好理解,就是在程序的一段代码片段中并不会导致引用关系发生变化,也就不用去更新OopMap表了,那么在这段代码区域内任何地方进行GC都是没有问题的。这段区域就称之为安全区域。线程执行的过程中,如果进入到安全区域内,就会标志自己已经进行到安全区域了。那么虚拟机要进行GC的时候,发现该线程已经运行到安全区域,就不会管该线程的死活了。所以,该线程在脱离安全区域的时候,要自己检查系统是否已经完成了GC或者根节点枚举(这个跟GC的算法有关系),如果完成了就继续执行,如果未完成,它就必须等待收到可以安全离开安全区域的Safe Region的信号为止。

猜你喜欢

转载自my.oschina.net/u/1757225/blog/1583822