safe point
在到达safe point后,jvm里的所有工作的应用线程都会被挂起,只有垃圾收集的native线程会持续不断地跑。
因为只有保证垃圾收集线程对jvm中地数据结构具有独占式的访问权限,它才能够做一些诸如标记GC Root 或是 移动堆中对象之类 或是 替换方法栈中的方法 之类的操作。
为何jvm要让应用线程进入safe point
- 停顿下来进行垃圾回收
- 指令重排以进行代码优化
- 清空代码缓存
- 类重新定义,类似于hot swap热部署
- 下载threadDump或者heapDump的时候
safe point问题定位
真正线上,除了GC,其他进入safe point的原因都可以不必关注。但如果真的发生了异常,可以使用如下参数进行调优:
-XX:+PrintGCApplicationStoppedTime – 这个不论是gc还是啥,只要进入了safe point,都会打印具体耗时。不幸的是它的输出结果缺少时间戳,但也足够我们缩小问题范围。
XX:+PrintGCApplicationConcurrentTime 和上面的命令相似但不同,它表示在两次应用线程进入safe point 之间,应用线程存活了多长时间。
-XX:+PrintSafepointStatistics –XX:PrintSafepointStatisticsCount=1 这两个选项会让jvm报告每次进入safe point的原因和耗时,并打印在stdout上,而非gc.log。
safe point工作原理
每个应用线程到达safe point后,都会安全地将自己挂起。jvm有两种字节码运行方式,预编译和即时编译(JIT),safe point机制需要保证在这两种方式下都能正常运行。
我们知道,java是一个典型的两段式编程语言,java编译器将项目源码编译为字节码,再由jvm运行字节码。jvm有两种方式运行字节码
- 一种是JIT方式(Just in time),也就是在运行的时候,即时地将bytecode转化为cpu可以识别的native指令。
- 一种是提前编译,将java代码直接编译成类似c++一样的语言,编译完后再运行。
JIT即时编译会在方法return后、调用结束后、跳出循环后等时机让应用线程进入safepoint,因为本着“不能让应用线程跑太久一直不进入safepoint”的原则,因为如果致大量线程一直无法进入safepoint,而GC线程也在等待这个应用线程进入safepoint才能开始GC,结果JVM就相当于被冻结了。
JIT编译的时候直接把safepoint的检查代码加入了生成的本地代码,当JVM需要让Java线程进入safepoint的时候,只需要设置一个标志位,让Java线程运行到safepoint的时候主动检查这个标志位,如果标志被设置,那么线程停顿,如果没有被设置,那么继续执行。
例如hotspot在x86中为轮询safepoint会生成一条类似于“test %eax,0x160100”的指令,JVM需要进入gc前,先把0x160100设置为不可读,那所有线程执行到检查0x160100的test指令后都会停顿下来。
0x01b6d627: call 0x01b2b210 ; OopMap{[60]=Oop off=460}; *invokeinterface size; - Client1::main@113 (line 23); {virtual_call}
0x01b6d62c: nop ; OopMap{[60]=Oop off=461};*if_icmplt; - Client1::main@118 (line 23)
0x01b6d62d: test %eax,0x160100 ; {poll}
0x01b6d633: mov 0x50(%esp),%esi
0x01b6d637: cmp %eax,%esi
在解释器执行方式下,JVM会设置一个2字节的dispatch tables,解释器执行的时候会经常去检查这个dispatch tables,当有safepoint请求的时候,就会让线程去进行safepoint检查。