来聊聊JVM中安全点的概念
文章目录
- 写在文章开头
- 详解safepoint基本概念
- 什么是安全点?为什么需要安全点
- JVM如何让线程跑到最近的安全点
- 线程什么时候需要进入安全点
- JVM如何保证线程高效进入安全点
- 如何设置安全点
- 用一次GC解释基于安全点的STW
- 实践-基于主线程休眠了解安全点的工作过程
- 代码示例
- 基于日志印证执行流程
- 优化思路
- 关于安全点更进一步的理解
- 关于安全点的调优建议
- JDK11对于安全点的优化
- RocketMQ中对于安全点的优化
- 小结
- 参考
写在文章开头
近期在分享关于synchronized
关键字的文章的时候提到了一个关于安全点的概念,有读者反馈这块知识点讲的有些潦草,遂以此文简单介绍一下JVM
中关于安全点的概念。
详解safepoint基本概念
什么是安全点?为什么需要安全点
在正式讲解安全点之前,我们不妨复习一下JVM
中垃圾回收的基本过程,我们以CMS
垃圾回收器为例,其垃圾回收过程在完成GC Roots
查找与收集之后就会按照如下步骤执行:
- 初始标记
- 并发回收
- 最终标记(重新标记)
- 并发清除
要知道固定可作为GC Roots
的节点主要是:
- 全局引用:例如常量或者静态变量。
- 执行上下文即栈帧中的变量表。
对于现代java
应用而言,光是方法区就可能有数百上千兆,所以对于这些起源的引用也并非一件容易的事情。这也就意味着JVM
在进行垃圾回收时并不能通过逐个扫描检查来实现。
就目前主流的JVM来说,针对根节点枚举基本都是采用空间换时间的策略,也就是使用一组OopMap
,全称为"Object Pointer Map"(对象指针映射)
,本质上就是一个位图索引,它会通过以下两个时机完成对象信息的缓存:
- 类加载完成后,
hotSpot
就会基于类的偏移量信息计算出来并缓存。 JIT
阶段也会在特定的时机(这一点后续会详细说明)
计算出栈或寄存器中的那些位置是引用,并将其缓存。
如此一来,下次进行根枚举时就可以直接基于OopMap
高效完成:
但是java进程的运行的瞬息万变的,可能此刻的对象在下一刻就不可用,下一刻又有新的对象诞生,这种引用关系的实时变化亦或者说导致OopMap
内容变化的指令是非常多的,若针对每一个指令都设置对应的oopMap
,那么内存的开销是非常高昂的。
所以就有了安全点(safepoint)
的概念,这也就是我们上文所提及的特定的位置
,基于这个设定,用户的程序仅仅会在特定的情况下生成oopMap
,同理在垃圾回收时,也要求所有线程达到安全点后才能够暂停并进入STW
从而开始进行初始标记、最终标记等操作:
例如下面这段代码:
Object o=new Object();
对应汇编码如下,可以看到0x00000000031ffb8f
的call
指令,它指明偏移量40-852处有一个普通对象指针Oop(Ordinary Object Pointer)
:
0x00000000031ffb80: mov $0xf5,%edx0x00000000031ffb85: mov %ecx,%ebp0x00000000031ffb87: mov %rbx,0x28(%rsp)0x00000000031ffb8c: data16 xchg %ax,%ax0x00000000031ffb8f: callq 0x00000000030957a0 ; OopMap{[40]=Oop off=852};*new ; - java.lang.String::<init>@58 (line 205); - java.lang.String::substring@52 (line 1933); {runtime_call}
JVM如何让线程跑到最近的安全点
对于安全点上的线程中断策略,大体来说是有两种:
- 抢占式:当需要进入安全点时,
JVM
会主动挂起所有的用户线程,如果线程未在安全点则等到该线程进入安全点进入安全点并完成中断。这种做法最大的缺点就是时间不可控即很可能存在性能不稳定亦或者吞吐量的波动,所以截至目前还有那款虚拟机采用抢占式的方式完成线程中断。 - 主动式:这种方式是让线程去维护一个标志位,需要进入安全点时修改该变量,用户线程就会在合适的时机检查这个变量值,如果这个值为真时就进入安全点。
线程什么时候需要进入安全点
除了常见的垃圾回收标记触发STW使得所有线程需要进入安全点以外,对应的进入安全点的时机还有:
- 使用
jstat
、jmap
、jstack
等命令,为保证监控堆栈信息的实时正确性,所有线程需要STW并进入安全点暂停。 - JDK8默认情况下定时进入安全点,保证一些需要进入安全点的操作能够及时运行。
- JIT编译代码优化例如:OSR(栈上替换即一种运行时替换栈帧的技术)或者去优化即Bailout(将JIT编译后的代码回退,解释器模式),因为可能存在执行指令的变化,线程就需要进入安全点。
- java agent需要对类进行增强导致类重新定义,需要修改类的相关信息,所以需要进入安全点。
- 高并发情况下,锁升级机制会涉及偏向锁撤销,需要进入STW所以也需要进入安全点。
JVM如何保证线程高效进入安全点
我们以线程运行JIT编译好的代码为例,它的设计与实现步骤为:
- JVM初始化一个异常处理器,专门捕获对应的
page fault
缺页中断异常。 - JIT编译代码期间,会基于我们上述的规则在特定位置插入一条精简的指令,作为安全点检查。
- VM线程通知当前线程进入安全点,将线程内部维护的内存页即
polling page
设置为不可读。 - 线程执行这条机器码指令发现内存页不可读,触发缺点中断。
- 异常处理器捕获这个异常,线程进入安全点。
对应的我们也给出这段精简的汇编码指令,即test %eax,0x160100 ; {poll}
这段指令,这段指令本质上就是执行poll操作检查安全点,尝试访问线程内存页对应地址为0x160100
,如果发现不可访问则触发缺页中断进入安全点:
0x01b6d627: call 0x01b2b210 ;<