Java ~ JVM ~ Safe Point

一 前言


    Java开发者首次接触到Safe Point(安全点)这个知识点大概率是在学习GC的时候,但实际上Safe Point(安全点)具有非常多的种类,除了GC Safe Point(GC安全点)之外常见的还有Biased Safe Point(偏向锁安全点)等等。虽说Safe Point(安全点)的种类繁杂,但基本上可以划分为“全局”与“局部”两类,这两类安全点的差别在于是否需要暂停所有的用户线程,即所谓的“Stop The World”,而我们接下来要讲述的GC Safe Point(GC安全点)就是一个典型的“全局”安全点。

    本文的内容携带了太多的个人主观猜测(相关位置会在文章中提及),因此只可作为参考,不可作为准确认知。若有同学发现本文的错误及疏漏,欢迎在评论区或私信告知,本人万分感谢!

二 GC Safe Point(GC安全点)概述


 Safe Point(安全点)

    在GC整体流程中,第一步是雷打不动的根节点枚举。在目前所有的收集器中,根节点枚举都必须要暂停所有用户线程,原因是如果该过程中根节点在不断的变化,将无法保证后续可达性分析的准确性。有同学可能会疑惑,虽然在根节点枚举时通过暂停所有用户线程保证了GC Roots(根节点集)的准确,但是后续的可达性分析却是并发的,GC Roots(根节点集)在这个过程中依然会发生变化,那不还是无法保证准确性吗?事实上确实如此,但这种情况是可以通过后续操作(增量更新、原始快照)进行弥补的,而如果GC Roots(根节点集)在一开始就不准确,那就无法弥补了。

    根节点枚举依赖于一组名为OopMap(Ordinary Object Pointer Map)的数据结构来获取GC Roots(根节点集)。实际上,OopMap并不是根节点枚举的必要条件,即使没有OopMap,GC也可以通过遍历所有的虚拟机栈及其它有关的内存区域来获取GC Roots(根节点集),但代价是枚举的时间会被拉的很长。因此可知,OopMap的作用是加速根节点枚举而不是实现根节点枚举

    OopMap不是本文的重点,此处不再详述,可以简单的理解为“记录了局部内存区域内哪些位置存有对象引用的数据集”的一致性快照。通过遍历所有的OopMap,就能够快速获取到整个GC Roots(根节点集)。理论上JVM每执行一条指令都能够触发生成新的OopMap,但后果是大量内存的额外消耗(因为会生成非常多的OopMap),使得空间成本变的无法忍受的高昂。为了避免这一点,JVM指定了在部分特定位置来生成OopMap(实际上就是选择了部分指令来触发生成OopMap),这些特定位置就是所谓的Safe Point(安全点)。

    Safe Point(安全点)的数量不能太少。太少会令用户线程长时间执行不到Safe Point(安全点)导致无法进行GC;但也不能太多,太多就会频繁的生成OopMap导致较高的内存负荷。因此Safe Point(安全点)的选择基本是以“是否具有令程序长时间执行的特征”为标准,所谓“长时间执行”最明显的特征就是指令序列复用(书上看来的,不懂什么意思,网上没查到,有知道的同学麻烦在评论区指教一下,感谢),例如方法调用、循环跳转、异常跳转等就属于指令序列复用,因此主要的Safe Point(安全点)有以下几种:

  • 方法返回之前;
  • 调用某个方法之后;
  • 抛出异常的位置;
  • 循环的末尾。

    Safe Point(安全点)的存在将方法分为了多段。当线程执行到Safe Point(安全点)时,会生成属于该段(即上个Safe Point(安全点)至当前Safe Point(安全点)之间的代码)的OopMap,并检查GC标志位决定是否进行暂停(HotSpot虚拟机使用的是主动式中断)。因此可知,Safe Point(安全点)不仅为OopMap的生成提供了时机,还为GC的执行提供了入口。但需要注意的是:Safe Point(安全点)与GC标志位检查点并不是对等的,还要加上所有的堆内存分配点。即Safe Point(安全点)是GC标志位检查点的子集。如果线程在执行至堆内存分配点时发现GC标志位为true,便会继续执行至最近的Safe Point(安全点)处暂停(生成OopMap后),直到所有的用户线程都暂停后开始GC。

 Safe Region(安全区域)

    Safe Point(安全点)机制并没有很好的解决用户线程暂停的问题。在我们目前的理解中,因为Safe Point(安全点)的存在,当需要进行GC时,用户线程不需要太多时间就能进入就近的Safe Point(安全点)处暂停。当所有的用户线程暂停后,GC自然也就开始了…但实际情况并非如此。上述机制只是保证了“执行”线程的正常运转…那如果线程不执行呢?

    所谓的不执行是指CPU没有给用户线程分配时间片,即用户线程处于一个无法获取CPU资源的状态。典型场景便如用户线程处于睡眠或阻塞状态,此时的线程无法响应JVM的中断请求(即无法到达GC标志位检查点进行检查),更无法执行至Safe Point(安全点)处进行挂起。显然JVM是不可能持续地对此类线程进行等待的…为了解决这个问题,JVM引入了Safe Region(安全区域)的概念。

    Safe Region(安全区域)可以看作被扩展拉伸了的安全点,是一段能够确保引用关系不会发生变化的代码片段(显然,像synchronized等代码都属于该范畴)。在Safe Region(安全区域)中的任意位置开始垃圾收集都是安全的。当用户线程执行到安全区域时,会对自己进行标记,这样当JVM发起GC时就不必去管这些已声明自己在Safe Region(安全区域)内的线程,即不会等待这些线程执行至Safe Point(安全点)。

    用户线程即将离开Safe Region(安全区域)前,会检查JVM是否已完成根节点枚举(或者GC过程中其它需要暂停用户线程的阶段)。如果完成,用户线程会在直接退出Safe Region(安全区域),并继续向下执行;否则便会一直等待,直至收到可以离开Safe Region(安全区域)的信号。

三 Biased Safe Point(偏向锁安全点)概述


 Safe Point(安全点)

    在synchronized的偏向锁机制中同样存在需要与Safe Point(安全点)相配合的操作,即偏向锁的撤销。与GC Safe Point(GC安全点)最大的区别在于,Biased Safe Point(偏向锁安全点)是一个“局部”安全点,即其并不需要暂停所有的用户线程,而只需暂停偏向线程即可。

    撤销偏向锁的请求由同时/获取竞争偏向锁失败的线程向JVM的任务队列发出,但执行则需要等到偏向线程达到Safe Point(安全点)处(如果偏向线程还存活的话,否则便无需等待。JVM维护了一个集合保存存活的线程,通过遍历该集合判断是否存活)由JVM线程(即处理专项任务的守护线程)负责执行。这是因为在撤销期间可能会修改偏向线程的虚拟机栈栈帧中的数据,因此需要在一个线程暂停的环境来避免并发问题。关于偏向线程是如何知道需要在Safe Point(安全点)处暂停的问题,目前没有查到准确资料,但参考GC的做法应该是会设置一个偏向锁撤销的标志位,当偏向线程在检查点发现偏向锁撤销的标志位为true时便会执行至最近的Safe Point(安全点)处暂停。而在偏向线程到达Safe Point(安全点)期间,其它竞争偏向锁的线程则会进行自旋等待。关于这一点其实也没有得到准确应征,只是在极少数的资料如此提及。但以个人认为可能性很大,毕竟在偏向锁层面不太可能会使用令线程休眠/等待等状态改变的操作,因此在未发现详细资料阐述这一点之前可以暂时如此理解。有知道的同学麻烦在评论区指教一下,感谢。可以发现,虽然网上关于偏向锁的文章数不胜数,但实际上内容都是十分粗略的。想要找到完整描述了偏向锁撤销完整流程资料基本没有可能,所以最好的方式还是去看JVM的源码,可惜本人是个C++的小白,所以只能从众多的“二手🍚”中尽可能连看带猜的拼凑出一个完整的流程。

    当JVM线程正式开始执行偏向锁撤销时,会如何处理其它竞争偏向锁呢?事实上不需要也不会去处理。因为偏向锁撤销并不会对这些线程的虚拟机栈做任何修改,并且你会发现synchronized关键字属于Safe Region(安全区域)的范畴,因此JVM会任由其继续自旋而不做任何操作。

四 参考文献


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说淑人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值