JVM:安全点的细节实现

这部分内容主要是为了稍后介绍各款垃圾收集器时做前置知识铺垫,如果对这部分内容感到枯燥或者疑惑,可以先放下看,等后续遇到要使用它们的实际场景、实际问题时再结合问题,再回来阅读和理解。

安全点

在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多(比如在一个执行方法中,随便new 一个对象赋值给一个变量),如果为每一条指令都生成对应的OopMap 记录,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂(这里有点不理解)。

实际上HotSpot也的确没有为每条指令都生成 OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。

有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。因此,安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的。(也就是具有长时间执行特性的就要在中间设置个安全点)

然而每条指令执行的时间实际上都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行,其实“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。

while (true) {// 例如这里的无限循环,每次循环末尾都会加入安全点
    
}
// ---------------------------------

// 然而类似于这种 counted loop(可数循环).循环内也没有调用别的方法,
// 由于所以它有可能被优化为循环末尾没有safepoint。
// 就有可能会导致进入safepoint非常耗时。(会导致偶尔GC 时停顿高)
for (int i = 1; i <= 1000000000; i++) {
        boolean b = 1.0 / i == 0;
}

// 优化和解决方案
// 优化方案一: 使用java1.8.131或者以上的版本, 在jvm运行参数中加-XX:+UseCountedLoopSafepoints
// 优化方案二: for循环中的int类型的i改变为long类型
// 原因:HotSpot虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,
// 所以使用int类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的
// 三:循环业务抽离

对于安全点,另外一个需要考虑的问题是,如何在垃圾收集发生时让所有线程都跑到最近的安全点,然后停顿下来

这里有两种方案可供选择:抢先式中断和主动式中断,

  1. 抢先式中断的思想是:不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地

方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。

  1. 主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。

由于轮询操作在代码中会频繁出现,这要求它必须足够高效。HotSpot使用内存保护陷阱的方式(这种思想应用还是挺广泛的觉得),把轮询操作精简至只有一条汇编指令的程度。当需要暂停用户线程时,虚拟机把轮询访问的内存页设置为不可读,那线程执行到test指令时就会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起线程实现等待,这样仅通过一条汇编指令便完成安全点轮询和触发线程中断了。

参考文章:https://zhuanlan.zhihu.com/p/286110609

安全区域

使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了,但实际情况却并不一定。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。

但是,程序“不执行”的时候呢?

所谓的程序不执行就是没有分配处理器时间,典型的场景就是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走

到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。

对于这种情况,就必须引入安全区域(Safe Region)来解决。

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举,如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。

你可能感兴趣的:(jvm)