写点什么

【新】虚拟机深层系列,java 底层实现原理

用户头像
极客good
关注
发布于: 刚刚

像 HotSpotVM 这样,在 Safepoint 会生成(polling 代码)主动请求询问 JVM 是否要进入 safepoint,polling 也有开销所以要尽量减少。


Native 代码的特殊性




当某个线程在执行 native 函数的时候。此时该线程在执行 JVM 管理之外的代码,不能对 JVM 的执行状态做任何修改,因而 JVM 要进入 safepoint 不需要关心它。


所以也可以把正在执行 native 函数的线程看作“已经进入了 safepoint”,或者把这种情况叫做“在 safe-region 里”。


JVM 外部要对 JVM 执行状态做修改必须要通过 JNI。所有能修改 JVM 执行状态的 JNI 函数在入口处都有 safepoint 检查,一旦 JVM 已经发出通知说此时应该已经到达 safepoint 就会在这些检查的地方停下来把控制权交给 JVM。


JRockit 选择放置 safepoint 的地方在方法的入口以及循环末尾回跳之前,跟 HotSpot 略为不同。


UseCountedLoopSafepoints:


可以避免 GC 发生时,线程因长时间运行 counted loop,进入不到 safepoint,而引起 GC 的 STW 时间过长。


JVM 参数-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1


在控制台输出以下信息:


`vmop [thread


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


s: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count 370337.312: GenCollectForAllocation [ 1070 2 3 ] [ 8830 0 8831 1 24 ]`


YGC 所花费的时间非常短,主要时间花费在所有线程达到安全点并暂停。


JVM 参数配置如下:




-server -Xms8192M -Xmx8192M -Xmn1500M -Xss256k -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:-UseBiasedLocking -XX:MonitorBound=16384 -XX:+UseSpinning -XX:PreBlockSpin=1 -XX:+UseParNewGC -XX:ParallelGCThreads=8 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=55 -XX:CMSMaxAbortablePrecleanTime=5 -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/xmail/jvm_heap.dump -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1


最有可能导致问题的是代码里有 Java 代码


for (int i = 0; i < ...; i++) { } 或者类似的循环代码。


这种循环称为“counted loop”,就是有明确的循环计数器变量,而且该变量有明确的起始值、终止值、步进长度的循环。


它有可能被优化为循环末尾没有 safepoint,于是如果这个循环的循环次数很多、循环体里又不调用别的方法或者是调用了方法但被内联进来了,就有可能会导致进入 safepoint 非常耗时。


可惜的是现在没什么特别方便的办法直接指出是什么地方有这种循环。有的话,一种解决办法是把单层循环拆成等价的双重嵌套循环,这样其中一层循环末尾的 safepoint 就可能会留下来,减少进入 safepoint 的等待时间。


如何判断内联方法




从代码角度如何判断方法被内联进来了,主要是方法被 final 修饰。 final 是可以帮助 JIT 编译器做出内联的判断,但不是必要条件。


  • -XX:+PrintCompilation -XX:+PrintInlining 来看内联状况

  • -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 ”输出的结果“[time: spin block sync cleanup vmop] ”中 spin 是指什么呢?

  • PrintSafepointStatics:打印出来的 spin 值指的是 SafepointSynchronize 在同步每个线程时做的自旋。


thread locking / biased locking 的 spin 完全没关系,自然设置那些参数也不会影响 safepoint 的自旋(UseSpinning 之类控制的是 thread locking 的自旋)。


SafePoint 存在的目的?




为什么把这些位置设置为 jvm 的安全点呢,主要目的就是避免程序长时间无法进入 safepoint,比如 JVM 在做 GC 之前要等所有的应用线程进入到安全点后 VM 线程才能分派 GC 任务 ,如果有线程一直没有进入到安全点,就会导致 GC 时 JVM 停顿时间延长。


比如,写了一个超大的循环导致线程一直没有进入到安全点,GC 前停顿了 8 秒。


产生的日志信息基本上 STW 的原因都是 RevokeBias 或者 BulkRevokeBias。这个是撤销偏向锁操作,虽然每次暂停的时间很短,但是特别频繁出现也会很耗时。


GC 如何找到不可用的对象


编写代码的时候是可以知道对象不可用的,但对于程序来说,需要一定的方式来知晓,可用方法比如:编译分析,引用计数,和对象是否可达。


可达性分析


因而可达性分析,只需要找到直接可达的引用,直接可达的引用就是根引用,根引用的集合就是根的集合


  1. 一个对象只要能够通过 mutator 触达,那么它就是“活”着的。

  2. 如果 Mutator 栈的一个槽位包含了对象的引用,那么对象就是直接可触达。

  3. 从直接可达对象可触达的对象必定也是可达的,


muator 线程分析


  • mutator 的上下文就包含了直接可达的数据,所以要获取对象根集合就是要找到 mutator 上下文中的对象引用,

  • mutator 的上下文指的就是它的栈、它的寄存器文件以及一些线程上特定的数据。


静态数据




全局数据本身也是直接可达的


可达性分析为了确保能正确的决定对象是否存活,GC 需要获取 mutator 上下文的(当前)一致性快照,然后枚举所有的根对象。


  • 一致性指的是:快照的抽取就像只在一个时间点发生,来避免丢失一些活着的对象。


如何获取 mutator 上下文的一致性快照


一种简单的方式就是在跟引用的过程中暂停所有的线程。当 mutator 暂停了它的执行时,只有将所有引用信息保存在其上下文中,才能枚举根的集合,这意味着,mutator 需要能够告诉 JVM 哪些栈的槽位有用,哪些寄存器持有引用。


如果 GC 能够准确的获取上述引用信息,它就称作精准根集合枚举。而无法获取就是不精准的。


如何获取精准的引用信息枚举




对于 java 来说,JIT 知晓所有的栈帧信息和寄存器的内容,当 JIT 编译一个方法时,对于每条指令,它都可以去保存根引用信息,保存意味着额外的存储空间,如果要存储所有的指令就显得花销太大,另外在真实的运行过程中也只有少数指令才会成为暂停点,因此 JIT 只需要保存这些指令点的信息就够了。而真正有机会成为暂停点的地方就称作 safe-points,即能够安全的枚举根集合的暂停点


如何保证 mutator 会在 safe-point 暂停




用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
【新】虚拟机深层系列,java底层实现原理