本博客主要是对文末列出的参考博客进行一个汇总整理,尽管也加上了一些个人的理解,但也不能算原创,但无奈csdn没有“整理”这种类型,因而还是挂成了原创。将这些零散的博客整理到一起有两方面的考虑:一方面是方便自己以后回顾,另一方面也方便大家进行系统性地学习。
JVM在很多场景下使用到safepoint,最常见的场景就是GC的时候。对一个Java线程来说,它要么处在safepoint,要么不在safepoint。下面列出了需要线程处于safepoint下才能执行的操作:
safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停暂停所以活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束。JVM维护了一个数据结构,记录了所有的线程,所以它可以快速检查所有线程的状态。当有GC请求时,所有进入到safepoint的Java线程会在一个Thread_Lock锁阻塞,直到当JVM操作完成后,VM释放Thread_Lock,阻塞的Java线程才能继续运行。
safepoint指的特定位置主要有:
为了让用户程序能正确地执行,JVM在背后需要维护各种数据信息,比如本文即将介绍的用于协助完成GC操作的OopMap,这些数据信息在修改前需要将其它所有线程在safepoint挂起,等到修改完成之后,再将所有线程恢复执行,以避免因线程并发而导致维护的数据不正确;此外,一些JVM操作,比如线程debug,以及线程栈导出等,这些操作也都需要将其它所有的线程在safepoint挂起,只剩下对应的线程来执行对应的操作。
GC的标记阶段需要stop the world,让所有Java线程挂起,这样JVM才可以安全地来标记对象。safepoint可以用来实现让所有Java线程挂起的需求。也就是说,执行GC的VMThread需要等待所有线程进入到safepoint之后才能进行。这意味着,如果有线程长时间运行而没有进入safepoint,那么GC也无法开始,JVM可能进入到“假死”状态,因为其它正常的线程都在阻塞在safepoint并等待被唤醒。
我们知道,JVM有两种执行方式:解释型和编译型(JIT),JVM要保证这两种执行方式下safepoint都能工作。
在JIT执行方式下,JIT编译的时候直接把safepoint的检查代码加入了生成的本地代码,当JVM需要让Java线程进入safepoint的时候,只需要设置一个标志位,让Java线程运行到safepoint的时候主动检查这个标志位,如果标志被设置,那么线程停顿,如果没有被设置,那么继续执行。
在解释器执行方式下,JVM会设置一个2字节的dispatch tables,解释器执行的时候会经常去检查这个dispatch tables,当有safepoint请求的时候,就会让线程去进行safepoint检查。
虽然前面提到在循环的末尾会插入safepoint,但参考博客8中提到JVM认为比较短的循环,比如以int作为index的循环,为了提高性能,是不会插入safepoint的,但我们知道,int的最大值是2147483647,因而正如该博客所说,当以int作为index的循环的上限值很大的时候,会导致虚拟机等待进入GC的耗时很长;而对于以long作为index的循环,则会在每次循环回跳之前插入safepoint,从而避免等待耗时长的问题。
参考博客8中还提到使用-XX:+UseCountedLoopSafepoints参数可以强制在Counted loop循环回跳之前插入Safepoint,也就是说即使循环比较短,JVM也会帮忙插入Safepoint了,用于防止大循环执行时间过长导致进入Safepoint卡住的问题。但是这个参数在JDK8上是有Bug的,可能会导致JVM Crash,而且是到JDK9才修复的。
safepoint只能处理正在运行的线程,它们可以主动运行到safepoint。而一些Sleep或者被blocked的线程不能主动运行到safepoint。这些线程也需要在GC的时候被标记检查,JVM引入了safe region的概念。safe region是指一块区域,这块区域中的引用都不会被修改,比如线程被阻塞了,那么它的线程堆栈中的引用是不会被修改的,JVM可以安全地进行标记。线程进入到safe region的时候先标识自己进入了safe region,等它被唤醒准备离开safe region的时候,先检查能否离开,如果GC已经完成,那么可以离开,否则就在safe region呆着。这可以理解,因为如果GC还没完成,那么这些在safe region中的线程也是被stop the world所影响的线程的一部分,如果让他们可以正常执行了,可能会影响标记的结果。
由参考博客1可知,gc roots包括了虚拟机线程栈(栈桢中的本地变量表)中的引用的对象,这意味着gc线程在垃圾收集时,需要对栈上的内存进行扫描,看看哪些位置存储了Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 Reference 类型的(它们是我们所需要的),那些非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。
一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 OopMap 的数据结构来记录这类信息。OopMap是ordinary object pointer map的简称,记录了栈上本地变量到堆上对象的引用关系。
我们知道,一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。
1、https://blog.csdn.net/Saintyyu/article/details/102882186 CMS垃圾回收器细节思考与补充
2、https://www.jianshu.com/p/c79c5e02ebe6 JVM源码分析之安全点safepoint
3、https://blog.csdn.net/iter_zc/article/details/41843595 聊聊JVM(五)从JVM角度理解线程
4、https://blog.csdn.net/iter_zc/article/details/41847887 聊聊JVM(六)理解JVM的safepoint
5、https://blog.csdn.net/iter_zc/article/details/41892567 聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞
6、https://blog.csdn.net/youyou1543724847/article/details/52728154 oopmap
7、https://stackoverflow.com/questions/20134769/how-to-get-java-stacks-when-jvm-cant-reach-a-safepoint
8、http://blog.itpub.net/31559359/viewspace-2650608/ HBase实战:记一次Safepoint导致长时间STW的踩坑之旅
9、https://www.iteye.com/blog/dsxwjhf-2201685 JVM 之 OopMap 和 RememberedSet
10、https://ask.csdn.net/questions/652787 为什么发生GC的时候要让所有的线程都运行到安全点(Safepoint)处呢
11、https://www.jianshu.com/p/e74fe532e35e jvm知识整理
12、https://www.cnblogs.com/vsop/p/10383944.html safepoint与UseCountedLoopSafepoints jdk 1.8.131之前会导致jvm crash
13、https://www.cnblogs.com/twoheads/p/10150063.html JVM锁简介:偏向锁、轻量级锁和重量级锁
14、https://blog.csdn.net/xw13106209/article/details/6989415 java native方法及JNI实例
15、https://blog.csdn.net/superfjj/article/details/107197580 小师妹学JVM之:JVM中的Safepoints :这些停下来的线程不包括运行native code的线程。因为这些native code是不属于JVM管理的。
16、https://blog.csdn.net/superfjj/article/details/107855767 JVM系列之:再谈java中的safepoint