HotSpot中的安全点详解

枚举根节点
目前主流的虚拟机都是采用准确式GC,当系统停顿下来时并不需要寻找每一个GC Roots。(stop the world:由于可达性分析对执行时间的敏感,只有在系统处于一个能确保一致性的快照中进行时才能有效,这时整个系统看上去好像被冻结了一样,不可以出现在分析过程中对象的引用关系还在发生变化的情况)实现方式是使用一组被称为oopMap的数据结构来达到这个目的。在类加载完成时,虚拟机就把对象内什么偏移量上是什么类型数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样GC在扫描时就能直接得知这些信息了。

OOPMAP
当类加载完成后,HotSpot 就将对象内存布局之中什么偏移量上数值是一个什么样的类型的数据这些信息存放到 OopMap 中;在 HotSpot 的 JIT 编译过程中,同样会插入相关指令来标明哪些位置存放的是对象引用等,这样在 GC 发生时,HotSpot 就可以直接扫描 OopMap 来获取这些信息,从而进行 GC Roots 枚举。在OoMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但有另一个问题,OopMap内容变化的指令非常多,如果为每一个条指令都生成对应的OopMap,将需要大量的额外空间。HotSpot没有为每条指令都生成OoMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点,

安全点是什么
OpenJDK官方定义如下:
安全点是在程序执行期间的所有GC Root已知并且所有堆对象的内容一致的点。
从全局的角度来看,所有线程必须在GC运行之前在安全点阻塞。 (作为一种特殊情况,运行JNI代码的线程可以继续运行,因为它们只使用句柄。但在安全点期间,它们必须阻塞而不是加载句柄的内容。)
从本地的角度来看,安全点是一个显着的点,它位于执行线程可能阻止GC的代码块中。 大多数调用点都能当做安全点。
在每个安全点都存在强大的不变量永远保持true不变,而在非安全点可能会被忽视。 编译的Java代码和C / C ++代码都在安全点之间进行了优化,但跨安全点时却不那么优化。 JIT编译器在每个安全点发出GC映射。 VM中的C / C ++代码使用程式化的基于宏的约定(例如,TRAPS)来标记潜在的安全点。
总的来说,安全点就是指,当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等

安全点位置
无论是哪种安全点,最简洁的定义是“A point in program where the state of execution is known by the VM”。不同JVM实现会选用不同的位置放置安全点。以HotSpot VM为例,在解释器里每条字节码的边界都可以是一个安全点,因为HotSpot的解释器总是能很容易的找出完整的“state of execution”。而在JIT编译的代码里,HotSpot会在所有方法的临返回之前,以及所有非counted loop的循环的回跳之前放置安全点。HotSpot的JIT编译器不但会生成机器码,还会额外在每个安全点生成一些“调试符号信息”,以便VM能找到所需的“state of execution”。为GC生成的符号信息是OopMap,指出栈上和寄存器里哪里有GC管理的指针;
为deoptimization生成的符号信息是debugInfo,指出如果要把当前栈帧从compiled frame转换为interpreted frame的话,要从哪里把相应的局部变量、临时变量、锁等信息找出来。之所以只在选定的位置放置安全点是因为:挂在安全点的调试符号信息要占用空间。如果允许每条机器码都可以是安全点的话,需要存储的数据量会很大(当然这有办法解决,例如用delta存储和用压缩)
安全点会影响优化。特别是deoptimization 安全点,会迫使JVM保留一些只有解释器可能需要的、JIT编译器认定无用的变量的值。本来JIT编译器可能可以发现某些值不需要而消除它们对应的运算,如果在安全点需要这些值的话那就只好保留了。这才是更重要的地方,所以要尽量少放置安全点像HotSpot VM这样,在安全点会生成polling代码询问VM是否要“进入安全点”,polling也有开销所以要尽量减少。安全点既不能太少,以至于 GC 过程等待程序到达安全点的时间过长,也不能太多,以至于 GC 过程带来的成本过高。

安全点上停止线程方式
一般会有两种解决方案:
抢先式中断:不需要线程的执行代码去主动配合,当发生 GC 时,先强制中断所有线程,然后如果发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断;这样一直等待所有线程都在安全点后开始 GC。
主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起;目前所有商用虚拟机全部采用主动式中断。

安全点总结
(1)一个线程可以在SafePoint上,也可以不在SafePoint上。一个线程在SafePoint时,它的状态可以安全地其他JVM线程所操作和观测;不在SafePoint时,就不能。
(2)在SafePoint上不代表被阻塞(比如:JNI方法就可以在SafePoint上运行),但是被阻塞一定发生在SafePoint上。
(3)当JVM决定达到一个全局的SafePoint(也叫做Stop the World)时,JVM里面所有的线程都要在SafePoint上并且不能离开,直到JVM让线程允许为止。这对要求所有线程都要被良好的描述的操作(比如CG,代码反优化等等)非常有好处。
(4)一些JVM可以持有一些私有的线程到SafePoint上而不需要全局的SafePoint,比如Zing.
(5)当你写一些非安全的代码的时候,你必须假设SafePoint有可能发生在任何两个字节码之间。
(6)非安全代码的调用并不要求必须有安全点,但是他们可以包含一个或者多个安全点。
(7)所有类型的JVM有一些效率非常高的技巧和去快速的穿过SafePoint,线程并不需要真正地进入SafePoint除非虚拟机指示线程这么做。
(8)所有的JNI方法都在SafePoint上执行。在安全点,JNI代码都不能改变和观测发起调用JNI代码线程的java机器状态。任何通过JNI API改变和观测调用线程的状态必须在调用线程离开安全点之后,以及再次进入SafePoint之前的时间内发生。

安全区(Saferegion)
安全点的机制似乎已经完美的解决了 “什么时候以及何时开始 GC” 的问题,但是实际情况并非如此;安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然 JVM 不可能一直等待被阻塞或休眠的线程正常唤醒执行;此时就引入了安全区的概念。
安全区(Saferegion):安全区域是指在一段区域内,对象引用关系等不会发生变化,在此区域内任意位置开始 GC 都是安全的;线程运行时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,JVM 发起 GC 时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了 GC Roots枚举(或这个GC过程),然后选择是否继续执行,否则将继续等待 GC 的完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值