☕【JVM 技术探索】史上最清晰的探究和分析【Safe Point+Safe Region】的原理和运行机制(上篇)
内容简介
之前写过一篇针对于 SafePoint 安全点的先关文章,主要针对于 SafePoint 的概念和定义以及相关作用做了相关的介绍,而且没有相关 SafeRegion 的说明和介绍,本篇文章主要是重塑和加深更加深层次的元 SafePoint 的原理和 SafeRegion 的原理进行整合和介绍。
安全点(Safe Point)
Java 程序执行时并非在所有地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来开始 GC,这些位置称为“安全点(Safepoint) ”
从线程的角度,安全点是代码执行中的一些特殊位置,当线程执行到这些特殊的位置,如果此时在 GC,那么在这个地方线程会暂停,阻塞住 Mutator 线程,直到 GC 结束。
GC 的时候要挂起所有活动 Mutator 的线程,因此线程挂起,会选择在到达安全点的时候挂起线程。。
安全点这个特殊的位置保存了线程上下文的全部信息。在进入安全点的时候打印日志信息能看出线程此刻都在干嘛。
安全点的选择
Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。。
比如:选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等。
线程中断类型
如何在 GC 发生时,检查所有线程都跑到最近的安全点停顿下来呢?
抢先式中断: (目前没有虚拟机采用了) 首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
主动式中断: 设置一个中断标志,各个线程运行到 Safe Point 的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
“A point in program where the state of execution is known by the VM”,即代码中 VM 能够准确知道执行状态的位置。
安全点分类
GC safepoint:要触发一次 GC,JVM 中的所有线程都必须达到 GC safepoint
Deoptimization safepoint,要触发一次 deoptimization,需要执行 deoptimization 的线程要到达 safepoint 之后才可以开始 deoptimize
Hotspot 中两者实现在一起,概念上没有直接联系,需要数据不一样
安全区域(Safe Region)
Safepoint 机制保证程序执行时,不长的时间内就会遇到可进入 GC 的 Safepoint
但是有时候并不是长时间的执行,而是长时间的空闲,比如 sleep、block,线程在执行其他的 native 函数,这些时候 JVM 无法掌控执行能力,也就无法响应 GC 事件。
如何解决 sleep/block 带来的问题
引用 safe-region。safe-region 是指代码快中没有用到会变异的部分,这样的代码块中,任何一个点都可以安全的枚举根。
当进入到 safe-region 中时,mutator 会设置一个准备标记,在离开 safe-region 区域之前,会检查 GC 是否已经完成了回收,如果没有,那么就暂停执行,如果有,就可以直接离开 safe-region 区域,不需要暂停 mutator
程序“不执行”的时候呢?例如线程处于 Sleep 状态或 Blocked 状态,这时候线程无法响应 JVM 的中断请求,“走” 到安全点去中断挂起,JVM 也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的。我们可以把 Safe Region 看做是被扩展了的 Safepoint。
程序实际执行时
当用户线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生 GC,JVM 会忽略标识为 Safe Region 状态的用户线程即用户线程 STW,等待 JVM 执行 GC 完毕;
当用户线程即将离开 Safe Region 时, 会检查 JVM 是否已经完成 GC,如果完成了,则用户线程继续运行,否则用户线程必须等待直到收到可以安全离开 SafeRegion 的信号为止;
可达性分析
GC 如何找到不可用的对象?编写代码的时候是可以知道对象不可用的,但对于程序来说,需要一定的方式来知晓,可用方法比如:【编译分析,引用计数,和对象是否可达】。
mutator 含义
一般 GC 执行完之后,才会恢复应用的运行,而在 GC 相关文献中,这里的应用就是 mutator 线程。
一个对象只要能够通过 mutator 触达,那么它就是“活”着的。
如果 Mutator 线程栈的槽位包含了对象的引用,那么对象就是直接可触达。
直接可达对象可触达的对象必定也是可达的,因而可达性分析,只需要找到直接可达的引用。
可达性引用
直接可达的引用就是根引用,根引用的集合就是根的集合。
Mutator 的上下文就包含了直接可达的数据,所以要获取对象根集合就是要找到 Mutator 上下文中的对象引用,而 mutator 的上下文指的就是它的栈、它的寄存器文件以及一些线程上特定的数据。
全局数据本身也是直接可达的
可达性分析为了确保能正确的决定对象是否存活,GC 需要获取 mutator 上下文的一致性快照,然后枚举所有 mutator 线程栈对应的 GCRoots 根对象。
如何获取 mutator 上下文的一致性快照
一致性指的是,快照的抽取就像只在一个时间点发生,来避免丢失一些活着的对象。
一种简单的方式就是在跟引用的过程中暂停所有的线程。当 mutator 暂停了它的执行时,只有将所有引用信息保存在其上下文中,才能枚举根的集合,这意味着,mutator 需要能够告知那些栈的槽位有引用,那些寄存器持有引用。
如果 GC 能够准确的获取上述引用信息,它就称作精准根集合枚举。
如何获取精准的引用信息枚举
对于 java 来说,JIT 知晓所有的栈帧信息和寄存器的内容,当 JIT 编译一个方法时,对于每条指令,它都可以去保存根引用信息,保存意味着额外的存储空间,如果要存储所有的指令就显得花销太大。
另外在真实的运行过程中也只有少数指令才会成为暂停点,因此 JIT 只需要保存这些指令点的信息就够了。而真正有机会成为暂停点的地方就称作 safe-points,即能够安全的枚举根集合的暂停点。
如何保证 mutator 会在 safe-point 暂停
当 GC 想要触发一次回收时,它会设置一个标志,mutator 则周期性的去检查(poll)这个标志,如果检查到了,就会立马暂停,这里的检查点(poll points)也是安全点,由 JIT 负责把【poll points】放到合适的位置(类似写屏障的感觉)
那些地方适合设置检查 GC 事件的标记
polling point 插入的主要原则是:
【polling point】应该足够多,防止 GC 等一个 mutator 的暂停太长,导致其他 mutator 都走在等 GC 释放空间,程序整个等待过长 polling point 不能太频繁导致运行时存储开销过大 polling 本身也是有开销的,不能过多权衡下来只在必须和必要的地方加在分配地址的时候强制添加,因为分配空间很有肯能导致回收,
所以这里是一个安全点长时间的执行一般意味着循环和方法调用,所以方法调用和循环返回最好加上>。
不同的 JVM 选用不同的位置放置 safepoint。
总结
代码的执行过程中,如果需要执行某些操作,比如 GC,deoptimize,等等,必须知道当前程序所有线程运行到的地方,是否能够恰好满足我执行对应操作,而不会对应用程序本身造成损害,这些能够正确执行操作的地方也就是 safepoint/saferegion
JVM 何时会回收方法区中的类元数据?
三个条件缺一不可:
类的所有实例(堆中)都已经被回收。
该类的 ClassLoader 已经被回收了。
该类对应的 Class 对象没有任何引用。
方法执行完毕,栈帧立马出栈,该栈帧中的变量数据立马回收?还是等垃圾回收器回收?为什么?
出栈就回收了,基础类型变量内存分配就在栈中,所以出栈就直接销毁了。引用堆中的对象需要等一次 YoungGC。
实例对象被回收’和‘Class 对象没有引用’ 是一个概念么?
不是,Class 对象代表的是类,如果有变量引用了类的 Class 对象,那么就是有引用。
新生代为何分为三块区域{Eden、From、To},半劈分成两块为什么不行?
三块区域,只有 From or To 空间是闲置的,而分为两块后,要有一半的新生代资源闲置着。
如何理解 STW 对系统的影响?调优策略如何制定?
一直以来都是想着控制 younggc 在 50ms 以下,oldgc 在 300ms 以下。但是 GC 执行势必都会带来 STW,JVM 分代回收的本质是对象的生命周期结束时就近一次执行 GC 进行回收。所以调优者需要估算出对象的生命周期。
parnew+cms 回收器,如何保证只做 younggc?
需要观察每秒钟新增多少对象,多长时间触发一次 younggc,平均一次 younggc 后有多少对象存活,survivor 区域是否放的下(对象动态年龄等问题),计算 survivor 区域与 eden 区域比例跳过动态年龄导致进入老年代的问题。
使用 ParNew 回收器并行线程是怎么设置的?
一般是与应用服务器的 CPU 核数保持一致。非要设定可以使用-XX:ParallelGCThreads 指定。
启动系统的时候是选择服务端模式还是客户端模式?对 ParNew 有什么影响?
系统部署在 linux 上就选择 server 模式,部署在 windows 上就选择 client 模式。一般 web 项目都是部署在多核的 linux 服务器上面,ParNew 可以充分利用多核资源。windows 上一般都是安装 client 模式,比如 qq、wx 等,如果用 ParNew 方式会导致 CPU 运行多个线程,反而加重了性能开销。所以 Client 一般选择 Serial 模式。
当 GC 发生时,每个线程只有进入了 SafePoint 才算是真正挂起,也就是真正的停顿,这个日志的含义是整个 GC 过程中 STW 的时间,配置了 -XX:+PrintGCApplicationStoppedTime 这个参数才会打印这个信息。
什么是 STW
等待所有用户线程进入安全点后并阻塞,做一些全局性操作的行为。
什么时候会 STW?
Garbage collection pauses(垃圾回收)
JIT 相关,比如 Code deoptimization, Flushing code cache
Class redefinition (e.g. javaagent,AOP 代码植入的产生的 instrumentation)
Biased lock revocation 取消偏向锁
Various debug operation (e.g. thread dump or deadlock check) dump 线程
STW 的说明
配置 -XX:+PrintSafepointStatistics –XX:PrintSafepointStatisticsCount=1 参数,虚拟机会打印如下日志文件:
-XX:+PrintSafepointStatistics,打印安全点统计信息,
-XX:PrintSafepointStatisticsCount=n 设置打印安全点统计信息的次数;
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/e277fe99fe76440768503939b】。文章转载请联系作者。
评论