JVM系列之:再谈java中的safepoint说明
safepoint是什么
java程序里面有很多很多的java线程,每个java线程又有自己的stack,并且共享了heap。这些线程一直运行呀运行,不断对stack和heap进行操作。
这个时候如果JVM需要对stack和heap做一些操作该怎么办呢?
比如JVM要进行GC操作,或者要做heapdump等等,这时候如果线程都在对stack或者heap进行修改,那么将不是一个稳定的状态。GC直接在这种情况下操作stack或者heap,会导致线程的异常。
怎么处理呢?
这个时候safepoint就出场了。
safepoint就是一个安全点,所有的线程执行到安全点的时候就会去检查是否需要执行safepoint操作,如果需要执行,那么所有的线程都将会等待,直到所有的线程进入safepoint。
然后JVM执行相应的操作之后,所有的线程再恢复执行。
safepoint的例子
我们举个例子,一般safepoint比如容易出现在循环遍历的情况,还是使用我们之前做null测试用的例子:
publicclassTestNull{ publicstaticvoidmain(String[]args)throwsInterruptedException{ Listlist=newArrayList(); list.add("www.flydean.com"); for(inti=0;i<10000;i++) { testMethod(list); } Thread.sleep(1000); } privatestaticvoidtestMethod(List list) { list.get(0); } }
运行结果如下:
标红的就是传说中的safepoint。
线程什么时候会进入safepoint
那么线程什么时候会进入safepoint呢?
一般来说,如果线程在竞争锁被阻塞,IO被阻塞,或者在等待获得监视器锁状态时,线程就处于safepoint状态。
如果线程再执行JNI代码的哪一个时刻,java线程也处于safepoint状态。因为java线程在执行本地代码之前,需要保存堆栈的状态,让后再移交给native方法。
如果java的字节码正在执行,那么我们不能判断该线程是不是在safepint上。
safepoint是怎么工作的
如果你使用的是hotspotJVM,那么这个safepoint是一个全局的safepoint,也就是说执行Safepoint需要暂停所有的线程。
如果你使用的是Zing,那么可以在线程级别使用safepoint。
我们可以看到生成的汇编语言中safepoint其实是一个test命令。
test指向的是一个特殊的内存页面地址,当JVM需要所有的线程都执行到safepint的时候,就会对该页面做一个标记。从而通知所有的线程。
我们再用一张图来详细说明:
thread1在收到设置safepoint之前是一直执行的,在收到信号之后还会执行一段时间,然后到达Safepint暂停执行。
thread2先执行了一段时间,然后因为CPU被抢夺,空闲了一段时间,在这段时间里面,thread2收到了设置safepoint的信号,然后thread2获得执行权力,接着继续执行,最后到达safepoint。
thread3是一个native方法,将会一直执行,知道safepoint结束。
thread4也是一个native方法,它和thread3的区别就在于,thread4在safepoint开始和结束之间结束了,需要将控制器转交给普通的java线程,因为这个时候JVM在执行Safepoint的操作,所以任然需要暂停执行。
在HotSpotVM中,你可以在汇编语言中看到safepoint的两种形式:'{poll}'或者‘{pollreturn}'。
总结
本文详细的讲解了JVM中Safepoint的作用,希望大家能够喜欢。
补充知识:JVM源码分析之安全点safepoint
上周有幸参加了一次关于JVM的小范围分享会,听完R大对虚拟机C2编译器的讲解,我的膝盖一直是肿的,能记住的实在有点少,能听进去也不多
1、什么时候进行C2编译,如何进行C2编译(这个实在太复杂)
2、C2编译的时候,是对整个方法体进行编译,而不是某个方法段
3、JVM中的safepoint
一直都知道,当发生GC时,正在执行Javacode的线程必须全部停下来,才可以进行垃圾回收,这就是熟悉的STW(stoptheworld),但是STW的背后实现原理,比如这些线程如何暂停、又如何恢复?就比较疑惑了。
然而这一切的一切,都涉及到一个概念safepoint,openjdk的实现位于
openjdk/hotspot/src/share/vm/runtime/safepoint.cpp
什么是safepoint
safepoint可以用在不同地方,比如GC、Deoptimization,在HotspotVM中,GCsafepoint比较常见,需要一个数据结构记录每个线程的调用栈、寄存器等一些重要的数据区域里什么地方包含了GC管理的指针。
从线程角度看,safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停暂停所以活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束。
什么地方可以放safepoint
下面以Hotspot为例,简单的说明一下什么地方会放置safepoint
1、理论上,在解释器的每条字节码的边界都可以放一个safepoint,不过挂在safepoint的调试符号信息要占用内存空间,如果每条机器码后面都加safepoint的话,需要保存大量的运行时数据,所以要尽量少放置safepoint,在safepoint会生成polling代码询问VM是否要“进入safepoint”,polling操作也是有开销的,polling操作会在后续解释。
2、通过JIT编译的代码里,会在所有方法的返回之前,以及所有非countedloop的循环(无界循环)回跳之前放置一个safepoint,为了防止发生GC需要STW时,该线程一直不能暂停。另外,JIT编译器在生成机器码的同时会为每个safepoint生成一些“调试符号信息”,为GC生成的符号信息是OopMap,指出栈上和寄存器里哪里有GC管理的指针。
线程如何被挂起
如果触发GC动作,VMthread会在VMThread::loop()方法中调用SafepointSynchronize::begin()方法,最终使所有的线程都进入到safepoint。
//Rollallthreadsforwardtoasafepointandsuspendthemall voidSafepointSynchronize::begin(){ Thread*myThread=Thread::current(); assert(myThread->is_VM_thread(),"OnlyVMthreadmayexecuteasafepoint"); if(PrintSafepointStatistics||PrintSafepointStatisticsTimeout>0){ _safepoint_begin_time=os::javaTimeNanos(); _ts_of_current_safepoint=tty->time_stamp().seconds(); }
在safepoint实现中,有这样一段注释,Javathreads可以有多种不同的状态,所以挂起的机制也不同,一共列举了5中情况:
1、执行Javacode
在执行字节码时会检查safepoint状态,因为在begin方法中会调用Interpreter::notice_safepoints()方法,通知解释器更新dispatchtable,实现如下:
voidTemplateInterpreter::notice_safepoints(){ if(!_notice_safepoints){ //switchtosafepointdispatchtable _notice_safepoints=true; copy_table((address*)&_safept_table,(address*)&_active_table,sizeof(_active_table)/sizeof(address)); } }
2、执行nativecode
如果VMthread发现一个Javathread正在执行nativecode,并不会等待该Javathread阻塞,不过当该Javathread从nativecode返回时,必须检查safepoint状态,看是否需要进行阻塞。
这里涉及到两个状态:Javathreadstate和safepointstate,两者之间有着严格的读写顺序,一般可以通过内存屏障实现,但是性能开销比较大,Hotspot采用另一种方式,调用os::serialize_thread_states()把每个线程的状态依次写入到同一个内存页中,实现如下:
//Serializeallthreadstatevariables voidos::serialize_thread_states(){ //OnsomeplatformssuchasSolaris&Linux,thetimedurationofthepage //permissionrestorationisobservedtobemuchlongerthanexpecteddueto //schedulerstarvationproblemetc.Toavoidthelongsynchronization //timeandexpensivepagetrapspinning,'SerializePageLock'isusedtoblock //themutatorthreadifsuchcaseisencountered.Seebug6546278fordetails. Thread::muxAcquire(&SerializePageLock,"serialize_thread_states"); os::protect_memory((char*)os::get_memory_serialize_page(), os::vm_page_size(),MEM_PROT_READ); os::protect_memory((char*)os::get_memory_serialize_page(), os::vm_page_size(),MEM_PROT_RW); Thread::muxRelease(&SerializePageLock); }
通过VMthread执行一系列mprotectoscall,保证之前所有线程状态的写入可以被顺序执行,效率更高。
3、执行compliedcode
如果想进入safepoint,则设置pollingpage不可读,当Javathread发现该内存页不可读时,最终会被阻塞挂起。在SafepointSynchronize::begin()方法中,通过os::make_polling_page_unreadable()方法设置pollingpage为不可读。
if(UseCompilerSafepoints&&DeferPollingPageLoopCount<0){ //Makepollingsafepointaware guarantee(PageArmed==0,"invariant"); PageArmed=1; os::make_polling_page_unreadable(); }
方法make_polling_page_unreadable()在不同系统的实现不一样
linux下实现
//Markthepollingpageasunreadable voidos::make_polling_page_unreadable(void){ if(!guard_memory((char*)_polling_page,Linux::page_size())) fatal("Couldnotdisablepollingpage"); };
solaris下实现
//Markthepollingpageasunreadable voidos::make_polling_page_unreadable(void){ if(mprotect((char*)_polling_page,page_size,PROT_NONE)!=0) fatal("Couldnotdisablepollingpage"); };
在JIT编译中,编译器会把safepoint检查的操作插入到机器码指令中,比如下面的指令:
0x01b6d627:call 0x01b2b210 ;OopMap{[60]=Oopoff=460}
;*invokeinterfacesize
;-Client1::main@113(line23)
; {virtual_call}
0x01b6d62c:nop ;OopMap{[60]=Oopoff=461}
;*if_icmplt
;-Client1::main@118(line23)
0x01b6d62d:test %eax,0x160100 ; {poll}
0x01b6d633:mov 0x50(%esp),%esi
0x01b6d637:cmp %eax,%esi
test%eax,0x160100就是一个检查pollingpage是否可读的操作,如果不可读,则该线程会被挂起等待。
4、线程处于Block状态
即使线程已经满足了blockcondition,也要等到safepointoperation完成,如GC操作,才能返回。
5、线程正在转换状态
会去检查safepoint状态,如果需要阻塞,就把自己挂起。
最终实现
当线程访问到被保护的内存地址时,会触发一个SIGSEGV信号,进而触发JVM的signalhandler来阻塞这个线程,TheGCthreadcanprotectsomememorytowhichallthreadsintheprocesscanwrite(usingthemprotectsystemcall)sotheynolongercan.Uponaccessingthistemporarilyforbiddenmemory,asignalhandlerkicksin。
再看看底层是如何处理这个SIGSEGV信号,实现位于
hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp //Checktoseeifwecaughtthesafepointcodeinthe //processofwriteprotectingthememoryserializationpage. //Itwriteenablesthepageimmediatelyafterprotectingit //sowecanjustreturntoretrythewrite. if((sig==SIGSEGV)&& os::is_memory_serialize_page(thread,(address)info->si_addr)){ //Blockcurrentthreaduntilthememoryserializepagepermissionrestored. os::block_on_serialize_page_trap(); returntrue; }
执行os::block_on_serialize_page_trap()把当前线程阻塞挂起。
线程如何恢复
有了begin方法,自然有对应的end方法,在SafepointSynchronize::end()中,会最终唤醒所有挂起等待的线程,大概实现如下:
1、重新设置poolingpage为可读
if(PageArmed){ //Makepollingsafepointaware os::make_polling_page_readable(); PageArmed=0; }
2、设置解释器为ignore_safepoints,实现如下:
//switchfromthedispatchtablewhichnoticessafepointsbacktothe //normaldispatchtable.Sothatwecannoticesinglesteppingpoints, //keepthesafepointdispatchtableifwearesinglesteppinginJVMTI. //Notethattheshould_post_single_steptestisexactlyasfastasthe //JvmtiExport::_enabledtestandcoversbothcases. voidTemplateInterpreter::ignore_safepoints(){ if(_notice_safepoints){ if(!JvmtiExport::should_post_single_step()){ //switchtonormaldispatchtable _notice_safepoints=false; copy_table((address*)&_normal_table,(address*)&_active_table,sizeof(_active_table)/sizeof(address)); } } }
3、唤醒所有挂起等待的线程
//Startsuspendedthreads for(JavaThread*current=Threads::first();current;current=current->next()){ //AproblemoccurringonSolarisiswhenattemptingtorestartthreads //thefirst#cpus-1gowell,butthentheVMThreadispreemptedwhenweget //tothenextone(sinceithasbeenrunningthelongest).Wethenhave //towaitforacputobecomeavailablebeforewecancontinuerestarting //threads. //FIXME:ThiscausestheperformanceoftheVMtodegradewhenactiveandwith //largenumbersofthreads.Apparentlythisisduetothesynchronousnature //ofsuspendingthreads. // //TODO-FIXME:thecommentsabovearevestigialandnolongerapply. //Furthermore,usingsolaris'schedctlinthisparticularcontextconfersnobenefit if(VMThreadHintNoPreempt){ os::hint_no_preempt(); } ThreadSafepointState*cur_state=current->safepoint_state(); assert(cur_state->type()!=ThreadSafepointState::_running,"Threadnotsuspendedatsafepoint"); cur_state->restart(); assert(cur_state->is_running(),"safepointstatehasnotbeenreset"); }
对JVM性能有什么影响
通过设置JVM参数-XX:+PrintGCApplicationStoppedTime,可以打出系统停止的时间,大概如下:
Totaltimeforwhichapplicationthreadswerestopped:0.0051000seconds Totaltimeforwhichapplicationthreadswerestopped:0.0041930seconds Totaltimeforwhichapplicationthreadswerestopped:0.0051210seconds Totaltimeforwhichapplicationthreadswerestopped:0.0050940seconds Totaltimeforwhichapplicationthreadswerestopped:0.0058720seconds Totaltimeforwhichapplicationthreadswerestopped:5.1298200seconds Totaltimeforwhichapplicationthreadswerestopped:0.0197290seconds Totaltimeforwhichapplicationthreadswerestopped:0.0087590seconds
从上面数据可以发现,有一次暂停时间特别长,达到了5秒多,这在线上环境肯定是无法忍受的,那么是什么原因导致的呢?
一个大概率的原因是当发生GC时,有线程迟迟进入不到safepoint进行阻塞,导致其他已经停止的线程也一直等待,VMThread也在等待所有的Java线程挂起才能开始GC,这里需要分析业务代码中是否存在有界的大循环逻辑,可能在JIT优化时,这些循环操作没有插入safepoint检查。
以上这篇JVM系列之:再谈java中的safepoint说明就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。