- Hotstpot safepoint介绍
1、什么是Safepoint
在hotstpot内部,有时候它会把 Java线程暂停掉,有时候又会把它叫做Stop The World,在hotstpot里可以做很多vm级别操作,如 GC、HeapDump/Stack trace、JVMTI、Check vmOperations.hpp,这里列了一个 vmOperations.hpp这个头文件里面列出了绝大部分的这些vm operation。下图演示,如正常的java的线程,运行的过程中,有一个VMthread,有些特殊的条件,触发了一个vm的操作请求,这时候就会发起一个请求,要求Java thread都进入safepoint, Java thread收到请求以后,会自己暂停,等所有的Java thread停下来,整个JVMTI都安全了,可以做一些比较复杂的vm的操作,等操作做完了以后,就可以要求这些Java线程再重新恢复。
举例来说像GC会把在Heap中的Java对象移来移去,如果这时Java线程正在运行的时候,一边运行对象一边移动,Java线程有可能就会访问到一个非法的地址,造成整个JVMTI的crash,所以这时候需要进入safepoint,把整个Java线程给暂停, Stop The World,会很影响性能。
2、Safepoint中还会做什么
从上述那些操作可知,在hotstpot中会做很很多事,平均下来,也许一秒钟之内会有两三次都会进入到一个safepoint,所以hotstpot会借用这个机会,用safepoint做一些常规的一些清理工作。
举例,如有些空的monitor,他觉得可以回收了,就可以把它回收到一个monitor的list,还有与inline cache相关的,会把它更新或者是清理掉。
还有些内部数据 stringtable或者symbol table这类数据结构,在safepoint中觉得可以有必要做一些rehash的操作的话也会在这里做,这些都是一些很短的操作,一般来说并不是特别需要关心,这里主要提一下,在进入safepoint的时候,hotstpot除了做vm operation以外,还会做一些这种常规的动作。
3、对Safepoint我们关注那些指标
safepoint会把整个jvm的那些应用线程给暂停掉这里主要是关心的当vm thread发出请求的时候,Java的实验者能够及时的响应safepoint的请求,能够马上的自己给停下来,如果有一些线程它停下来了,另一些线程还在运行,这样的话其他的线程就会等于是浪费时间在等待,所以说及时响应是它一个很重要的指标。
进入了safepoint后, vm operation它本身操作,也希望能够在尽快短的时间内完成,完成了以后,还要能够快速的退出,这里一般没有问题,因为safepoint的退出都比较简单,一般来说不太会造成什么影响,前面三个点从进到做vm operation和退出,整个是影响了一次暂停的时间,如果你业务方比较关心这种延迟、响应时间这些指标的话,也许就要关注这几个性能。
有可能进一次safepoint很短,很快,但是safepoint的发生的时间频率又很高,这样的话,就会发现它总体暂停的时间就会很长,所以频率和总体时间也是一个需要关注的指标,如果对应用的吞吐量和性能比较关注的话,就要关注safepoint的总的暂停时间和它的那些频率,这里就是对safepoint有可能要关注一些性能。
4、Safepoint内部实现
safepoint采用的是一种协作式的方式,就是当它发起了safepoint的请求后,那些Java线程来检测这个请求,然后再把自己给暂停,而不是通过强迫式,例如VMthread调用某一个API强行把一个thread给占进,强行暂停也许可以很快的把种线程给暂停住,但是这里会有很多不确定的状态在里面,安全性就很容易形成问题。
Hotspot是所以就采用了这种协作式的方式,每个Java线程它能够及时的判断出来 safepoint的请求,能够到一个他自认为可以安全的一个点上把自己给停下来。
与此同时,既然是协作式,就是说这些Java线程怎样能够确保它能够及时的响应,因为有可能在做自己很复杂的业务逻辑,什么时候去检查 safepoint,做这么多的检查,会不会影响到 Java本身的性能,这些都是需要综合考虑的一件事。
5、Java thread状态转换
在Hotspot里,对于这种Java的线程,其实主要有三个状态,在互相这样转换,第一个就是说是Thread in Java,这个是说明这个线程现在执行的代码是Java的代码,如下图中标注,在执行Java代码中,在hotstpot里它其实又分成两种模式,一种是解释器模式就interpreter,第二种是JIT,生成的那种native的code,这两种模式它在这个里面处理也是不一样的。
另外两了状态Thread in native和Thread in VM,他们其实执行的都是类似于像c和c++的一些代码。
Thread in vm的话主要是hotspot本身自己的那些代码;
Thread initiative的话主要是一些JMI,如Java code有的时候需要调一些GMI的接口去访问,去调用一些c的库和方法,这时候它会进入的是Thread in native的状态。
以上就是他的三个状态,在 safepoint的时候,要针对这三种不同的情况来做不同的处理。
6、Thread in vm
Thread in vm主要执行的像hotstpot内部代码,如arraycopy,如现在要执行一个arraycopy拷贝到一半的时候,GC如果把array移到另一个位置,肯定就出问题了,拷贝的都是一个非法的数据,做arraycopy的时候,其实是会把自己Java线程的状态标志为Thread in VM,类似的像反射,有的时候做一些resolve或link,hottsport里有很多的这种操作,因为这些动作它往往是直接去操作hot stpot内部的那些数据结构,所以不会希望有一些vm operation类似像GC那些动作,来做这些事情,所以需要用把线程状态标志为Thread in VM,在Thread in VM的状态下,这个时候VM thread必须要等这个操作给做完以后才能做,所以hot stpot里对这些在VM状态的代码,其实做得很小心,它必须要保证这些这些事情能够很快的完成,不会有这种长时间的阻塞或者这样的动作。
7、Thread in native
Thread in native其实是通过JMI接口去执行了 c和c++的一些native的code,在这种状态下,其实在JMI中已经认为它进入了safepoint,即使已经在运行,与前面提到的stop the world好像理解上有点不一样,这时候这个线程其实还是可以一直在运行的,因为如果这个代码是native的code,其实hotstpot是没法知道是什么状态的,而且也没法控制行为,有可能在做一个很长的 Loop,在那里不停的执行,所以这个时候如果要等的话,肯定会出问题safepoint就进不去了,但这时候认为已经是safepoint了,就可以做那些vm operation,因为我的Java线上还在运行,当 native code执行自己的东西的时候,是不会去碰到那些Java内部的那些hip hop object的那些东西,当想访问那些object的时候,需要通过那些JMI的接口,当调用接口的时候,这个时候JVM就会来检查这时候是不是正在做safepoint,如果正在做safepoint,就会把调用给阻塞,然后线程就会被停下来,等vm operation结束了以后再继续执行下去。
所以虽然在Thread in native状态你仍然在运行,但实际上不会造成造成危害,因为要访问那种Java object或者访问hip的时候,这里的JMI接口会挡住。
8、Thread in java-interpreter
Thread in Java的解释器模式,hotstpot中解释器其实是通过一个叫dispatch table的一个数据结构来实现的,Dispatch table就是一个很大的 table,对于每个bite code,它对应的就是一小段的执行代码,所以它执行的时候,是哪个bite code就执行 dispatch table中的哪一段代码,然后在不停的跳转。
在解释器里面,在hotstpot中,其实是维护了两套dispatch table,一个就是normal table,这就是刚刚说对每个 bite code做解释执行的代码,另一个safept table,除了做正常的解释执行之外,对每个bite code执行之前会加入一小段代码来检测,Jvm是不是发起了 safepoint的请求,如果发起safepoint的请求,就可以把自己给停下来。
通过这样一个方式来safepoint的check的,正常的话, Java执行的都是normal table里的bite code,如果 vm Thread决定发起一次safepoint的请求的时候,hotstpot内部有个active table的指针,它会做一次切换,从normal table中切换到了safept table。
一个bite code执行完,会去取下一次bite code的执行代码,因为这时候已经被切换到了safept table,会执行ssafept table中对应的代码,然后就会检查safepoint,然后再暂停。
所以基本上可以理解在解释器模式中,在每一次的bite code的最后都会做一次检查,但实际上它是通过一个 table的表的一个切换来做的,正常运行的话,其实并没有做检查,所以它的性能并不会受影响。
9、Thread in java-jit
Jet最关注的是它的性能,在jet生成的code中,如何来检查safepoint,在hotstpot里,在它启动的时候,会先申请一个全局的polling page的这一个页,是一个4k大小的页,然后在jit生成的代码中,在某些特定的一个点,它会生成一两条指令,直接去访问页,就去读一下页里面这个内容是不是可读,特定点大概有两个,第一个是在jit code的返回的时候,在return的地方会去检查一次;另一个是循环,如果代码里面有循环,它会在循环的 loop的back edge中,他\也会去检查一次,只在这两个点上去做检查,一方面是确保他\检查尽可能的少,另一方面要确保它的jit能够及时的响应 safepoint的请求,本身只是读一下,并没有做任何的动作,这里如何把自己给停下来,就是 vm Thread开始要触发sfepoint的时候,会做一个动作,会把全局的pulling page把他的权限给改了,会用n protect类似于的API把权限设成不可访问。
这样如果读取polling page的这条指令就会触发一次SIGSEGV的异常,但 hotstpot本身在 signal handle里面,会对这种SIGSEGV做进行一些特殊处理,它会捕获住这种异常,会看触发异常的地址,是不是polling page ,然后如果是个polling page的,就知道是jit里面触发的 safepoint,所以这里并不是一个真正的异常,而是一次safepoint的请求。
后续的操作,会把 Java线程给暂停,然后把自己的状态标志为已经进入了 safepoint。
如下图所示这段jit深层的代码,里面有一个 Loop的polling,又有一个 return的polling,可以看这两条test的指令,用红框标出来的,最上面的是一个 polling是一个在back edge中他用来做polling的,其实只是做一次test,把 polling地址放到了20寄存器中,然后就去读一下test一下,后续对这个其实根本没有任何操作, Test的结果对他来说没有任何作用,就是为了去读一次,能读这个代码就可以继续往下执行。
下面的一条test,旁边的标注是poll return,紧接着下面就是一个return的指令,所以这一条指令就是在return之前,也会去做一次polling,来判断下是不是有人在发起了 safepoint的请求。
这就是在jit code中,大概会在这样的两个地方去做 polling,第二个test,如果看上一条,可能会看到20的地址其实是从二十五中读取了一个偏移量过来,25在现在X86的hotstpot,主要是用来做一个thread,所以它其实是从thread中去读了一个。
这里说明一下,牵涉到新的一个 jdk10引入了一个技术,引入了一个叫thread local handkerchief,因为上述的 polling page是global的,实际上把 global的page把它作为这个地址记下来,然后每次polling的时候就直接去访问这个地址,这就是一个常量,根本没有任何动作不需要去到thread上去读。
10、Global polling vs Thread Local handshake
在jdk10它这里引入了一个叫thread the local的hand shake,这是一个新的协议品,主要的一个目的是要能够对一个特定的thread来触发safepoint,前面讲过触发safepoint以后是会让所有的线程都停下来,但对某些操作,也许只是对一个线程来做动作的话,做一个把整个 Java线程全部停下来的操作,是一个比较比较浪费的一个行为。
所以希望就是说能够用 thread local的机制,只对一个特定的thread来把它给暂停,在11里面,都是用thread local,这时候他取polling page的时候,都是从通过自己的thread里面去读一个polling page的地址。
实际上怎么做到thread local,其实上述的polling page中,做了两个页,一个就是好的每次都能读,另一个是坏的,读就肯定会失败,good page和bad page这样两个页,所以如果要对某个线程进行暂停的话,进入safepoint的话,其实就是把线程上的page的地址改写一下,改成坏页,这样 thread就会触发到异常来进入safepoint,这里有一个开关,叫User ThreadLocal Handshakes,它现在默认是打开的,基本上默认都会去走thread local的 safepoint,如果还是想用global pulling,可以把它关掉。
实际上用到thread local,用特定的线程来进入safepoint的这种win其实也没有多少,主要是现在的cgc大概会用到它。
Jit因为比较关注性能,如果那种loop在一个循环里面,每个loop的回编中都要去做一个 polling,虽然只是一两条指令,但如果是在一个大循环里面,加起来的性能其实还是会有影响的,所以hotstpot为了提高它的性能,可以把counted loop的polling给去掉,counted loop就是一般看到的for loop,可以认为是那种for的循环,因为这种循环中会有一个循环变量,循环变量有初始值,有它的边界,有它的布长,基本上都是固定的,在hotstpot里面,就会认为这种循环叫counted loop,在counted loop里面hotstpot可以做一个优化,把这种 polling的指令去掉,来提高它的性能,但这样会造成它的一个trade off,如果你的counted loop比较大,这样进safepoint的时间就会就会被推迟了。
因为在整个循环中都不会去检查polling,都不会去检查safepoint,要等这个循环执行完一直到最后退出的时候,才会检查,造成的一个可能负面影响,就是说对进safepoint的时间它会延迟掉。
像G1/ZGC一些新的GC,这些机器更关注的是说暂停的时间,为了要把暂停时间给减少,所以这些GC的时候,又会默认把 counted loop中的pulling给生成出来。
总的开关,就是UseCountedLoopSafepoints ,打开就会生成,关掉就不生成这些polling。
11、监控safepoint
在日常的维护中,一般来说希望能知道safepoint究竟造成了一些行为是怎样的,这里提供的一些选项,像JDK8,主要是提供了,能够打印safepoint的统计信息,能够知道它大概发生了多少次,总的暂停时间,可以计算一下它的平均时间等。
但在JDK11中,已经把这一个选项基本上已经是废弃了,因为在JDK11中,已经用了一个新的一套Log的机制,这套Log机制中对safepoint就可以用这个命令 logsafepoint=debug打开这个开关,会打印出很多的跟safepoint的详细信息,如进入safepoint的花了多少时间,出来大概多少时间,总的时间是多少,这些详细的这些信息都能够在用 log来记,所以在JDK11中,其实是比较推荐用这种方式来看safepoint的这些数据。