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

1.OopMapjava

以前咱们提到,在正式的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的信号为止。

相关文章
相关标签/搜索