内容简介
之前写过一篇针对于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设置打印安全点统计信息的次数;