JVM源碼分析之安全點(diǎn)safepoint

簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處,謝謝!

上周有幸參加了一次關(guān)于JVM的小范圍分享會(huì),聽完R大對虛擬機(jī)C2編譯器的講解,我的膝蓋一直是腫的,能記住的實(shí)在有點(diǎn)少,能聽進(jìn)去也不多
1、什么時(shí)候進(jìn)行C2編譯,如何進(jìn)行C2編譯(這個(gè)實(shí)在太復(fù)雜)
2、C2編譯的時(shí)候,是對整個(gè)方法體進(jìn)行編譯,而不是某個(gè)方法段
3、JVM中的safepoint

一直都知道,當(dāng)發(fā)生GC時(shí),正在執(zhí)行Java code的線程必須全部停下來,才可以進(jìn)行垃圾回收,這就是熟悉的STW(stop the world),但是STW的背后實(shí)現(xiàn)原理,比如這些線程如何暫停、又如何恢復(fù)?就比較疑惑了。

然而這一切的一切,都涉及到一個(gè)概念safepoint,openjdk的實(shí)現(xiàn)位于openjdk/hotspot/src/share/vm/runtime/safepoint.cpp

什么是safepoint

safepoint可以用在不同地方,比如GC、Deoptimization,在Hotspot VM中,GC safepoint比較常見,需要一個(gè)數(shù)據(jù)結(jié)構(gòu)記錄每個(gè)線程的調(diào)用棧、寄存器等一些重要的數(shù)據(jù)區(qū)域里什么地方包含了GC管理的指針。

從線程角度看,safepoint可以理解成是在代碼執(zhí)行過程中的一些特殊位置,當(dāng)線程執(zhí)行到這些位置的時(shí)候,說明虛擬機(jī)當(dāng)前的狀態(tài)是安全的,如果有需要,可以在這個(gè)位置暫停,比如發(fā)生GC時(shí),需要暫停暫停所以活動(dòng)線程,但是線程在這個(gè)時(shí)刻,還沒有執(zhí)行到一個(gè)安全點(diǎn),所以該線程應(yīng)該繼續(xù)執(zhí)行,到達(dá)下一個(gè)安全點(diǎn)的時(shí)候暫停,等待GC結(jié)束。

什么地方可以放safepoint

下面以Hotspot為例,簡單的說明一下什么地方會(huì)放置safepoint
1、理論上,在解釋器的每條字節(jié)碼的邊界都可以放一個(gè)safepoint,不過掛在safepoint的調(diào)試符號信息要占用內(nèi)存空間,如果每條機(jī)器碼后面都加safepoint的話,需要保存大量的運(yùn)行時(shí)數(shù)據(jù),所以要盡量少放置safepoint,在safepoint會(huì)生成polling代碼詢問VM是否要“進(jìn)入safepoint”,polling操作也是有開銷的,polling操作會(huì)在后續(xù)解釋。

2、通過JIT編譯的代碼里,會(huì)在所有方法的返回之前,以及所有非counted loop的循環(huán)(無界循環(huán))回跳之前放置一個(gè)safepoint,為了防止發(fā)生GC需要STW時(shí),該線程一直不能暫停。另外,JIT編譯器在生成機(jī)器碼的同時(shí)會(huì)為每個(gè)safepoint生成一些“調(diào)試符號信息”,為GC生成的符號信息是OopMap,指出棧上和寄存器里哪里有GC管理的指針。

線程如何被掛起

如果觸發(fā)GC動(dòng)作,VM thread會(huì)在VMThread::loop()方法中調(diào)用SafepointSynchronize::begin()方法,最終使所有的線程都進(jìn)入到safepoint。

// Roll all threads forward to a safepoint and suspend them all
void SafepointSynchronize::begin() {
  Thread* myThread = Thread::current();
  assert(myThread->is_VM_thread(), "Only VM thread may execute a safepoint");

  if (PrintSafepointStatistics || PrintSafepointStatisticsTimeout > 0) {
    _safepoint_begin_time = os::javaTimeNanos();
    _ts_of_current_safepoint = tty->time_stamp().seconds();
  }

在safepoint實(shí)現(xiàn)中,有這樣一段注釋,Java threads可以有多種不同的狀態(tài),所以掛起的機(jī)制也不同,一共列舉了5中情況:


1、執(zhí)行Java code

在執(zhí)行字節(jié)碼時(shí)會(huì)檢查safepoint狀態(tài),因?yàn)樵?code>begin方法中會(huì)調(diào)用Interpreter::notice_safepoints()方法,通知解釋器更新dispatch table,實(shí)現(xiàn)如下:

void TemplateInterpreter::notice_safepoints() {
  if (!_notice_safepoints) {
    // switch to safepoint dispatch table
    _notice_safepoints = true;
    copy_table((address*)&_safept_table, (address*)&_active_table, sizeof(_active_table) / sizeof(address));
  }
}

2、執(zhí)行native code

如果VM thread發(fā)現(xiàn)一個(gè)Java thread正在執(zhí)行native code,并不會(huì)等待該Java thread阻塞,不過當(dāng)該Java thread從native code返回時(shí),必須檢查safepoint狀態(tài),看是否需要進(jìn)行阻塞。

這里涉及到兩個(gè)狀態(tài):Java thread state和safepoint state,兩者之間有著嚴(yán)格的讀寫順序,一般可以通過內(nèi)存屏障實(shí)現(xiàn),但是性能開銷比較大,Hotspot采用另一種方式,調(diào)用os::serialize_thread_states()把每個(gè)線程的狀態(tài)依次寫入到同一個(gè)內(nèi)存頁中,實(shí)現(xiàn)如下:

// Serialize all thread state variables
void os::serialize_thread_states() {
  // On some platforms such as Solaris & Linux, the time duration of the page
  // permission restoration is observed to be much longer than expected  due to
  // scheduler starvation problem etc. To avoid the long synchronization
  // time and expensive page trap spinning, 'SerializePageLock' is used to block
  // the mutator thread if such case is encountered. See bug 6546278 for details.
  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);
}

通過VM thread執(zhí)行一系列mprotect os call,保證之前所有線程狀態(tài)的寫入可以被順序執(zhí)行,效率更高。

3、執(zhí)行complied code

如果想進(jìn)入safepoint,則設(shè)置polling page不可讀,當(dāng)Java thread發(fā)現(xiàn)該內(nèi)存頁不可讀時(shí),最終會(huì)被阻塞掛起。在SafepointSynchronize::begin()方法中,通過os::make_polling_page_unreadable()方法設(shè)置polling page為不可讀。

if (UseCompilerSafepoints && DeferPollingPageLoopCount < 0) {
    // Make polling safepoint aware
    guarantee (PageArmed == 0, "invariant") ;
    PageArmed = 1 ;
    os::make_polling_page_unreadable();
}

方法make_polling_page_unreadable()在不同系統(tǒng)的實(shí)現(xiàn)不一樣

linux下實(shí)現(xiàn)
// Mark the polling page as unreadable
void os::make_polling_page_unreadable(void) {
  if( !guard_memory((char*)_polling_page, Linux::page_size()) )
    fatal("Could not disable polling page");
};

solaris下實(shí)現(xiàn)
// Mark the polling page as unreadable
void os::make_polling_page_unreadable(void) {
  if( mprotect((char *)_polling_page, page_size, PROT_NONE) != 0 )
    fatal("Could not disable polling page");
};

在JIT編譯中,編譯器會(huì)把safepoint檢查的操作插入到機(jī)器碼指令中,比如下面的指令:

0x01b6d627: call   0x01b2b210         ; OopMap{[60]=Oop off=460}      
                                       ;*invokeinterface size      
                                       ; - Client1::main@113 (line 23)      
                                       ;   {virtual_call}      
 0x01b6d62c: nop                       ; OopMap{[60]=Oop off=461}      
                                       ;*if_icmplt      
                                       ; - Client1::main@118 (line 23)      
 0x01b6d62d: test   %eax,0x160100      ;   {poll}      
 0x01b6d633: mov    0x50(%esp),%esi      
 0x01b6d637: cmp    %eax,%esi     

test %eax,0x160100 就是一個(gè)檢查polling page是否可讀的操作,如果不可讀,則該線程會(huì)被掛起等待。

4、線程處于Block狀態(tài)

即使線程已經(jīng)滿足了block condition,也要等到safepoint operation完成,如GC操作,才能返回。

5、線程正在轉(zhuǎn)換狀態(tài)

會(huì)去檢查safepoint狀態(tài),如果需要阻塞,就把自己掛起。

最終實(shí)現(xiàn)

當(dāng)線程訪問到被保護(hù)的內(nèi)存地址時(shí),會(huì)觸發(fā)一個(gè)SIGSEGV信號,進(jìn)而觸發(fā)JVM的signal handler來阻塞這個(gè)線程,The GC thread can protect some memory to which all threads in the process can write (using the mprotect system call) so they no longer can. Upon accessing this temporarily forbidden memory, a signal handler kicks in。

再看看底層是如何處理這個(gè)SIGSEGV信號,實(shí)現(xiàn)位于hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp

// Check to see if we caught the safepoint code in the
// process of write protecting the memory serialization page.
// It write enables the page immediately after protecting it
// so we can just return to retry the write.
if ((sig == SIGSEGV) &&
    os::is_memory_serialize_page(thread, (address) info->si_addr)) {
  // Block current thread until the memory serialize page permission restored.
  os::block_on_serialize_page_trap();
  return true;
}

執(zhí)行os::block_on_serialize_page_trap()把當(dāng)前線程阻塞掛起。

線程如何恢復(fù)

有了begin方法,自然有對應(yīng)的end方法,在SafepointSynchronize::end()中,會(huì)最終喚醒所有掛起等待的線程,大概實(shí)現(xiàn)如下:
1、重新設(shè)置pooling page為可讀

  if (PageArmed) {
    // Make polling safepoint aware
    os::make_polling_page_readable();
    PageArmed = 0 ;
  }

2、設(shè)置解釋器為ignore_safepoints,實(shí)現(xiàn)如下:

// switch from the dispatch table which notices safepoints back to the
// normal dispatch table.  So that we can notice single stepping points,
// keep the safepoint dispatch table if we are single stepping in JVMTI.
// Note that the should_post_single_step test is exactly as fast as the
// JvmtiExport::_enabled test and covers both cases.
void TemplateInterpreter::ignore_safepoints() {
  if (_notice_safepoints) {
    if (!JvmtiExport::should_post_single_step()) {
      // switch to normal dispatch table
      _notice_safepoints = false;
      copy_table((address*)&_normal_table, (address*)&_active_table, sizeof(_active_table) / sizeof(address));
    }
  }
}

3、喚醒所有掛起等待的線程

// Start suspended threads
    for(JavaThread *current = Threads::first(); current; current = current->next()) {
      // A problem occurring on Solaris is when attempting to restart threads
      // the first #cpus - 1 go well, but then the VMThread is preempted when we get
      // to the next one (since it has been running the longest).  We then have
      // to wait for a cpu to become available before we can continue restarting
      // threads.
      // FIXME: This causes the performance of the VM to degrade when active and with
      // large numbers of threads.  Apparently this is due to the synchronous nature
      // of suspending threads.
      //
      // TODO-FIXME: the comments above are vestigial and no longer apply.
      // Furthermore, using solaris' schedctl in this particular context confers no benefit
      if (VMThreadHintNoPreempt) {
        os::hint_no_preempt();
      }
      ThreadSafepointState* cur_state = current->safepoint_state();
      assert(cur_state->type() != ThreadSafepointState::_running, "Thread not suspended at safepoint");
      cur_state->restart();
      assert(cur_state->is_running(), "safepoint state has not been reset");
    }

對JVM性能有什么影響

通過設(shè)置JVM參數(shù) -XX:+PrintGCApplicationStoppedTime, 可以打出系統(tǒng)停止的時(shí)間,大概如下:

Total time for which application threads were stopped: 0.0051000 seconds  
Total time for which application threads were stopped: 0.0041930 seconds  
Total time for which application threads were stopped: 0.0051210 seconds  
Total time for which application threads were stopped: 0.0050940 seconds  
Total time for which application threads were stopped: 0.0058720 seconds  
Total time for which application threads were stopped: 5.1298200 seconds
Total time for which application threads were stopped: 0.0197290 seconds  
Total time for which application threads were stopped: 0.0087590 seconds   

從上面數(shù)據(jù)可以發(fā)現(xiàn),有一次暫停時(shí)間特別長,達(dá)到了5秒多,這在線上環(huán)境肯定是無法忍受的,那么是什么原因?qū)е碌哪兀?/p>

一個(gè)大概率的原因是當(dāng)發(fā)生GC時(shí),有線程遲遲進(jìn)入不到safepoint進(jìn)行阻塞,導(dǎo)致其他已經(jīng)停止的線程也一直等待,VM Thread也在等待所有的Java線程掛起才能開始GC,這里需要分析業(yè)務(wù)代碼中是否存在有界的大循環(huán)邏輯,可能在JIT優(yōu)化時(shí),這些循環(huán)操作沒有插入safepoint檢查。

參考資料:
聊聊JVM(九)理解進(jìn)入safepoint時(shí)如何讓Java線程全部阻塞
現(xiàn)代JVM中的Safe Region和Safe Point到底是如何定義和劃分的?

How to get Java stacks when JVM can't reach a safepoint
Safepoints in HotSpot JVM

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容