解讀HotSpot 安全點(safepoint)技術

?

安全點(safepoint)在HotSpot中是一個核心的技術點,所謂安全點,指的是代碼執行過程中被選擇出來的一些位置,當JVM需要執行一些要STW(Stop The World)的操作的時候,這些位置用于線程進入這些位置并等待系統執行完成STW操作;安全點不能太少也不能太多,安全點過少會導致那些需要執行STW操作的程序需要等待太久,安全點太多又會導致程序執行時需要頻繁的check安全點,導致安全點check造成系統負載升高。在HotSpot內部,需要STW的操作典型的是GC(garbage collect),這一點很好理解,如果在GC的時候用戶線程還在執行,那么用戶線程就會產生新的垃圾對象,這部分對象我們稱為“浮動垃圾”,當然,運行的用戶線程除了產生新的垃圾,還會引用原本被GC標記為垃圾的對象,這樣GC的時候就會錯誤的將還有引用關系的對象回收掉,最終導致程序錯誤,所以,在GC的時候,是需要STW的,當然,隨著GC技術的發展,一些GC(比如CMS等)都可以和用戶線程并發執行,但這其實只是將GC分階段執行,在其中的某些階段和用戶程序并發執行,當真正執行垃圾回收的時候還是需要STW的,比如CMS,就把垃圾回收分成了初始標記、并發標記、最終標記及并發清除四個階段,其中,初始標記和最終標記還是需要STW的。除了GC,還有其他的操作需要STW的嗎?答案是肯定的。在文章 Java 動態調試技術原理及實踐 中,提到了JVM運行時類重定義,文章提到實現了一種可以進行動態調試的Java-debug-tool工具,根據描述,這種工具使用了jvmti(JVM Tool Interface)實現了java agent,并可以動態的掛載到目標JVM上,執行命令,然后在運行時類中插樁并重新加載類,使得新加載的類中的方法執行時可以產生大量的調試信息。在HotSpot中,這種技術的實現和GC一樣,也需要STW(相對應的VM_Operation是VM_RedefineClasses),但是本文重點在于解讀safepoint的相關實現細節,所以對于VM_RedefineClasses以及VM_Operation的相關內容不做過多描述。在HotSpot中,SafepointSynchronize用來實現安全點,下面是典型的進入和退出安全點的代碼片段:

// ... enter the safepoint
SafepointSynchronize::begin();
?
// do the stw work here
?
// ... exit the safepoint
SafepointSynchronize::end();

SafepointSynchronize::begin用于實現進入安全點,當所有線程都進入了安全點后,VMThread才能繼續執行后面的代碼,SafepointSynchronize::end用于實現退出安全點。進入安全點時java線程可能存在不同的狀態,這里需要處理所有的可能情況:

  • (1)處于解釋執行字節碼的狀態中,解釋器在通過字節碼派發表(dispatch table)獲取到下一條字節碼的時候會主動檢查安全點的狀態;

  • (2)處于執行native代碼的狀態,也就是執行JNI,此時,VMThread不會等待線程進入安全點,執行JNI退出后線程需要主動檢查安全點狀態,如果此時安全點位置被標記了,那么就不能繼續執行,需要等待安全點位置被清除后才能繼續執行;

  • (3)處于編譯代碼執行中,那么編譯器會在合適的位置(比如循環、方法調用等)插入讀取全局Safepoint Polling內存頁的指令,如果此時安全點位置被標記了,那么Safepoint Polling內存頁會變成不可讀,此時線程會因為讀取了不可讀的內存也而陷入內核,事先注冊好的信號處理程序就會處理這個信號并讓線程進入安全點。

  • (4)線程本身處于blocked狀態,比如線程在等待鎖,那么線程的阻塞狀態將不會結束直到安全點標志被清除掉;

  • (5)當線程處于(1)/(2)/(3)三種狀態的切換中,那么切換前會先檢查安全點的狀態,如果此時要求進入安全點,那么切換將不被允許,需要等待直到安全點狀態清除;

SafepointSynchronize::begin進入安全點


下面就來看看HotSpot進入安全點的函數begin的實現細節:

// Roll all threads forward to a safepoint and suspend them all
void SafepointSynchronize::begin() {
  
  // 1
  Thread* myThread = Thread::current();
  assert(myThread->is_VM_thread(), "Only VM thread may execute a safepoint");
?
  // 2
  // By getting the Threads_lock, we assure that no threads are about to start or
  // exit. It is released again in SafepointSynchronize::end().
  Threads_lock->lock();
?
  // 3
  assert( _state == _not_synchronized, "trying to safepoint synchronize with wrong state");
?
  // 4
  int nof_threads = Threads::number_of_threads();
?
  // 5
  MutexLocker mu(Safepoint_lock);
?
  // 6
  // Reset the count of active JNI critical threads
  _current_jni_active_count = 0;
?
  // 7
  // Set number of threads to wait for, before we initiate the callbacks
  _waiting_to_block = nof_threads;
  
  // 8
  TryingToBlock     = 0 ;
  
  // 9
  int still_running = nof_threads;
?
  // Begin the process of bringing the system to a safepoint.
  // Java threads can be in several different states and are
  // stopped by different mechanisms:
  //
  //  1. Running interpreted
  //     The interpreter dispatch table is changed to force it to
  //     check for a safepoint condition between bytecodes.
  //  2. Running in native code
  //     When returning from the native code, a Java thread must check
  //     the safepoint _state to see if we must block.  If the
  //     VM thread sees a Java thread in native, it does
  //     not wait for this thread to block.  The order of the memory
  //     writes and reads of both the safepoint state and the Java
  //     threads state is critical.  In order to guarantee that the
  //     memory writes are serialized with respect to each other,
  //     the VM thread issues a memory barrier instruction
  //     (on MP systems).  In order to avoid the overhead of issuing
  //     a memory barrier for each Java thread making native calls, each Java
  //     thread performs a write to a single memory page after changing
  //     the thread state.  The VM thread performs a sequence of
  //     mprotect OS calls which forces all previous writes from all
  //     Java threads to be serialized.  This is done in the
  //     os::serialize_thread_states() call.  This has proven to be
  //     much more efficient than executing a membar instruction
  //     on every call to native code.
  //  3. Running compiled Code
  //     Compiled code reads a global (Safepoint Polling) page that
  //     is set to fault if we are trying to get to a safepoint.
  //  4. Blocked
  //     A thread which is blocked will not be allowed to return from the
  //     block condition until the safepoint operation is complete.
  //  5. In VM or Transitioning between states
  //     If a Java thread is currently running in the VM or transitioning
  //     between states, the safepointing code will wait for the thread to
  //     block itself when it attempts transitions to a new state.
  //
  {
    EventSafepointStateSynchronization sync_event;
    int initial_running = 0;
?
    // 10
    _state            = _synchronizing;
    
    // 11
    OrderAccess::fence();
?
    // 13
    // Flush all thread states to memory
    if (!UseMembar) {
      os::serialize_thread_states();
    }
?
    // 14
    // Make interpreter safepoint aware
    Interpreter::notice_safepoints();
?
    // 15
    os::make_polling_page_unreadable();
?
    // 16
    // Consider using active_processor_count() ... but that call is expensive.
    int ncpus = os::processor_count() ;
?
    // 17
    // Iterate through all threads until it have been determined how to stop them all at a safepoint
    unsigned int iterations = 0;
    int steps = 0 ;
    
    // 18
    while(still_running > 0) {
      // 19
      for (JavaThread *cur = Threads::first(); cur != NULL; cur = cur->next()) {
        
        // 20
        ThreadSafepointState *cur_state = cur->safepoint_state();
        
        // 21
        if (cur_state->is_running()) {
          
          // 22
          cur_state->examine_state_of_thread();
          
          // 23
          if (!cur_state->is_running()) {
            
            // 24
            still_running--;
            // consider adjusting steps downward:
            //   steps = 0
            //   steps -= NNN
            //   steps >>= 1
            //   steps = MIN(steps, 2000-100)
            //   if (iterations != 0) steps -= NNN
          }
        }
      }
?
      // 25
      if (still_running > 0) {
?
        // Spin to avoid context switching.
        // There's a tension between allowing the mutators to run (and rendezvous)
        // vs spinning.  As the VM thread spins, wasting cycles, it consumes CPU that
        // a mutator might otherwise use profitably to reach a safepoint.  Excessive
        // spinning by the VM thread on a saturated system can increase rendezvous latency.
        // Blocking or yielding incur their own penalties in the form of context switching
        // and the resultant loss of $ residency.
        //
        // Further complicating matters is that yield() does not work as naively expected
        // on many platforms -- yield() does not guarantee that any other ready threads
        // will run.   As such we revert to naked_short_sleep() after some number of iterations.
        // nakes_short_sleep() is implemented as a short unconditional sleep.
        // Typical operating systems round a "short" sleep period up to 10 msecs, so sleeping
        // can actually increase the time it takes the VM thread to detect that a system-wide
        // stop-the-world safepoint has been reached.  In a pathological scenario such as that
        // described in CR6415670 the VMthread may sleep just before the mutator(s) become safe.
        // In that case the mutators will be stalled waiting for the safepoint to complete and the
        // the VMthread will be sleeping, waiting for the mutators to rendezvous.  The VMthread
        // will eventually wake up and detect that all mutators are safe, at which point
        // we'll again make progress.
        //
        // Beware too that that the VMThread typically runs at elevated priority.
        // Its default priority is higher than the default mutator priority.
        // Obviously, this complicates spinning.
        //
        // Note too that on Windows XP SwitchThreadTo() has quite different behavior than Sleep(0).
        // Sleep(0) will _not yield to lower priority threads, while SwitchThreadTo() will.
        //
        // See the comments in synchronizer.cpp for additional remarks on spinning.
        //
        // In the future we might:
        // 1. Modify the safepoint scheme to avoid potentially unbounded spinning.
        //    This is tricky as the path used by a thread exiting the JVM (say on
        //    on JNI call-out) simply stores into its state field.  The burden
        //    is placed on the VM thread, which must poll (spin).
        // 2. Find something useful to do while spinning.  If the safepoint is GC-related
        //    we might aggressively scan the stacks of threads that are already safe.
        // 3. Use Solaris schedctl to examine the state of the still-running mutators.
        //    If all the mutators are ONPROC there's no reason to sleep or yield.
        // 4. YieldTo() any still-running mutators that are ready but OFFPROC.
        // 5. Check system saturation.  If the system is not fully saturated then
        //    simply spin and avoid sleep/yield.
        // 6. As still-running mutators rendezvous they could unpark the sleeping
        //    VMthread.  This works well for still-running mutators that become
        //    safe.  The VMthread must still poll for mutators that call-out.
        // 7. Drive the policy on time-since-begin instead of iterations.
        // 8. Consider making the spin duration a function of the # of CPUs:
        //    Spin = (((ncpus-1) * M) + K) + F(still_running)
        //    Alternately, instead of counting iterations of the outer loop
        //    we could count the # of threads visited in the inner loop, above.
        // 9. On windows consider using the return value from SwitchThreadTo()
        //    to drive subsequent spin/SwitchThreadTo()/Sleep(N) decisions.
        
        // 26
        os::make_polling_page_unreadable();
?
        // 27
        // Instead of (ncpus > 1) consider either (still_running < (ncpus + EPSILON)) or
        // ((still_running + _waiting_to_block - TryingToBlock)) < ncpus)
        ++steps ;
        if (ncpus > 1 && steps < SafepointSpinBeforeYield) {
          SpinPause() ;     // MP-Polite spin
        } else
          if (steps < DeferThrSuspendLoopCount) {
            os::naked_yield() ;
          } else {
            os::naked_short_sleep(1);
          }
?
        // 28
        iterations ++ ;
      }
    }
    
    // 29
    assert(still_running == 0, "sanity check");
  } //EventSafepointStateSync
?
  // wait until all threads are stopped
  {
    
    // 30
    int initial_waiting_to_block = _waiting_to_block;
?
    // 31
    while (_waiting_to_block > 0) {
      if (!SafepointTimeout || timeout_error_printed) {
        Safepoint_lock->wait(true);  // true, means with no safepoint checks
      } else {
        // Compute remaining time
        jlong remaining_time = safepoint_limit_time - os::javaTimeNanos();
?
        // If there is no remaining time, then there is an error
        if (remaining_time < 0 || Safepoint_lock->wait(true, remaining_time / MICROUNITS)) {
          print_safepoint_timeout(_blocking_timeout);
        }
      }
    }
    
    // 32
    assert(_waiting_to_block == 0, "sanity check");
?
    // 33
    // Record state
    _state = _synchronized;
?
    // 34
    OrderAccess::fence();
  } // EventSafepointWaitBlocked
}

進入安全點的代碼,下面按照注釋的34個點進行解釋:

  • (1)獲取當前線程,并且判斷是否是VMThread,VMThread才能執行安全點代碼,這里強制校驗一下;

  • (2)獲取到全局線程鎖,這樣在安全點就沒有線程可以start或者exit;

  • (3)安全點同步狀態,一共有三個狀態,0表示線程都不再安全點上,1表示正在讓線程運行到安全點,2表示所有線程均已進入安全點,這里判斷了一下,如果此時已經為3,那么就沒必要執行同步安全點的工作了;

  enum SynchronizeState {
      _not_synchronized = 0,                   // Threads not synchronized at a safepoint
                                               // Keep this value 0. See the comment in do_call_back()
      _synchronizing    = 1,                   // Synchronizing in progress
      _synchronized     = 2                    // All Java threads are stopped at a safepoint. Only VM thread is running
  };
  • (4)獲取到當前JVM內的線程數量;

  • (5)獲取到安全點執行鎖,這樣其他線程(比如CMS GC線程)就無法執行安全點代碼,保證并發安全;

  • (6)重置處于JNI代碼執行的線程數,VMThread不會等待處于JNI的線程,這些線程需要主動check安全點,這個計數器將在那些執行JNI的線程退出時被更新,這個在后面將block的時候再將;

  • (7)需要等待進入安全點的線程數量;

  • (8)嘗試阻塞次數;

  • (9)依然還在運行的線程數量;

  • (10)將同步狀態改成_synchronizing,表示正在同步安全點;

  • (11)刷新高速緩存,完成多核將數據同步;

  • (13)刷新線程的狀態到內存;

  • (14)通知字節碼解釋器進入安全點,這樣解釋器將會在執行下一條字節碼執行的時候check安全點并進入安全點,這一點后面再詳細描述;

  • (15)讓全局safepoint polling內存頁不可讀,這樣,執行那些被編譯成本地代碼的指令的過程中,線程就會因為讀到不可讀的頁面而陷入內核,之后就會進入安全點;

  • (16)獲取當前機器CPU核數;

  • (17)一共等待的輪數,用于spin;

  • (18)只要還有線程正在運行,那么就要繼續迭代,直到所有線程都進入安全點;

  • (19)循環的看每一個線程;

  • (20)獲取當前線程的ThreadSafePointState,這個狀態會在線程創建的時候創建;

  • (21)如果當前還沒到安全點,那么就要讓他進入安全點;

  • (22)examine_state_of_thread函數用于check當前線程是否已經進入安全點狀態,如果進入了,就需要更新一些計數:

void ThreadSafepointState::examine_state_of_thread() {
 // 1 判斷當前狀態
 assert(is_running(), "better be running or just have hit safepoint poll");
?
 // 2 獲取當前狀態
 JavaThreadState state = _thread->thread_state();
?
 // Save the state at the start of safepoint processing.
 _orig_thread_state = state;
?
 // Check for a thread that is suspended. Note that thread resume tries
 // to grab the Threads_lock which we own here, so a thread cannot be
 // resumed during safepoint synchronization.
?
 // We check to see if this thread is suspended without locking to
 // avoid deadlocking with a third thread that is waiting for this
 // thread to be suspended. The third thread can notice the safepoint
 // that we're trying to start at the beginning of its SR_lock->wait()
 // call. If that happens, then the third thread will block on the
 // safepoint while still holding the underlying SR_lock. We won't be
 // able to get the SR_lock and we'll deadlock.
 //
 // We don't need to grab the SR_lock here for two reasons:
 // 1) The suspend flags are both volatile and are set with an
 //    Atomic::cmpxchg() call so we should see the suspended
 //    state right away.
 // 2) We're being called from the safepoint polling loop; if
 //    we don't see the suspended state on this iteration, then
 //    we'll come around again.
 //
 // 看是否已經掛起
 bool is_suspended = _thread->is_ext_suspended();
 if (is_suspended) {
?
   // 如果線程已經掛起,那么就更新狀態為_at_safepoint
   roll_forward(_at_safepoint);
   return;
 }
?
 // Some JavaThread states have an initial safepoint state of
 // running, but are actually at a safepoint. We will happily
 // agree and update the safepoint state here.
 // 有些線程正在執行JNI,此時雖然線程狀態是running的,但是不需要等待進入安全點,那么也可以直接更新狀態
 if (SafepointSynchronize::safepoint_safe(_thread, state)) {
   SafepointSynchronize::check_for_lazy_critical_native(_thread, state);
?
   // 更新狀態
   roll_forward(_at_safepoint);
   return;
 }
?
 // 如果線程正在執行java代碼,那么就標記為需要進入安全點,后續線程進入安全點的時候會更新這個狀態
 if (state == _thread_in_vm) {
   roll_forward(_call_back);
   return;
 }
?
 // All other thread states will continue to run until they
 // transition and self-block in state _blocked
 // Safepoint polling in compiled code causes the Java threads to do the same.
 // Note: new threads may require a malloc so they must be allowed to finish
?
 assert(is_running(), "examine_state_of_thread on non-running thread");
 return;
}
   ```

   roll_forward就和它的函數名字一樣,要推進線程進入安全點的進程:

```java
// Returns true is thread could not be rolled forward at present position.
void ThreadSafepointState::roll_forward(suspend_type type) {
 _type = type;
?
 switch(_type) {
   // 如果進入安全了
   case _at_safepoint:
     
     // 執行_waiting_to_block--操作,完成當前線程進入安全點的工作
     SafepointSynchronize::signal_thread_at_safepoint();
     
     // 如果當前線程正在執行native代碼,執行_current_jni_active_count++
     if (_thread->in_critical()) {
       // Notice that this thread is in a critical section
       SafepointSynchronize::increment_jni_active_count();
     }
     break;
?
   // 還沒有達到安全點,那么就要標記一下,等待線程進入安全點   
   case _call_back:
     set_has_called_back(false);
     break;
 }
}
  • (23、24)再次檢測當前線程是否已經進入安全點,如果是的話,那么就更新still_running計數器;

  • (25)完成一輪檢測之后,判斷是否還存在沒有進入安全點的狀態,如果有的話,需要繼續執行;

  • (26)這里其實是有參數控制經過多少次后再將safepoint polling頁設置為不可讀的,所以這里還有一個設置操作,但是為了代碼簡潔一些,我把那些計數器去掉了;

  • (27)如果經過了太多論迭代還是沒能讓線程進入安全點,考慮到CPU消耗,可以適當等一會再輪詢;

  • (28)迭代次數更新;

  • (29)循環結束后,表示所有線程都進入了安全點,此時still_running應該為0;

  • (30、31、32)等待_waiting_to_block為0;

  • (33)將狀態變更為_synchronized,表示進入安全點結束;

  • (34)刷新緩存;

SafepointSynchronize::end退出安全點


相應的,有進入安全點的代碼,就有退出安全點的代碼,進入安全點的時候,VMThread使得所有線程都進入了block狀態,那退出安全點的時候,VMThread就有責任將所有線程喚醒,讓他們繼續執行接下來的代碼,下面就來看看end函數的實現細節:

// Wake up all threads, so they are ready to resume execution after the safepoint
// operation has been carried out
void SafepointSynchronize::end() {
  // memory fence isn't required here since an odd _safepoint_counter
  // value can do no harm and a fence is issued below anyway.
?
  // 1
  assert(myThread->is_VM_thread(), "Only VM thread can execute a safepoint");
?
  // 2
  // Make polling safepoint aware
  os::make_polling_page_readable();
?
  // 3
  // Remove safepoint check from interpreter
  Interpreter::ignore_safepoints();
?
  {
    // 4
    MutexLocker mu(Safepoint_lock);
?
    // 5
    assert(_state == _synchronized, "must be synchronized before ending safepoint synchronization");
?
    // 6
    // Set to not synchronized, so the threads will not go into the signal_thread_blocked method
    // when they get restarted.
    _state = _not_synchronized;
    
    // 7
    OrderAccess::fence();
?
    // 8
    // 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
?
      // 9
      ThreadSafepointState* cur_state = current->safepoint_state();
      
      // 10
      assert(cur_state->type() != ThreadSafepointState::_running, "Thread not suspended at safepoint");
      
      // 11
      cur_state->restart();
      
      // 12
      assert(cur_state->is_running(), "safepoint state has not been reset");
    }
?
    // 13
    // Release threads lock, so threads can be created/destroyed again. It will also starts all threads
    // blocked in signal_thread_blocked
    Threads_lock->unlock();
  }
}
  • (1)還是要判斷一下,只有VMThread才能執行安全點代碼;

  • (2)這里要讓safepoint polling內存頁重新變為可讀,如果這里不變更,那么執行那些被編譯為本地代碼的代碼時就會陷入內核,無法繼續執行,這和進入安全點時的讓這個頁面不可讀是對偶的;

  • (3)告訴字節碼解釋器退出安全點了,這一點下面再花篇幅來解讀;

  • (4)獲取safepoint鎖,并發安全;

  • (5)執行退出時狀態應該是所有線程都已經同步,否則就是錯誤的狀態;

  • (6)變更狀態,重點在于后面變為所有線程都離開了安全點;

  • (7)告訴緩存刷新;

  • (8)循環讓每個線程都離開安全點,重新運行;

  • (9)獲取當前線程的安全點狀態;

  • (10)判斷線程狀態,如果此時線程的安全點狀態已經在處于running了,那么就說明程序出錯了;

  • (11)調用restart函數,這里其實只是做安全點狀態的變化,沒有特別復雜;

  • (12)restart會將安全點狀態變為running,這里校驗一下;

  • (13)釋放線程鎖,進入安全點的時候獲取到了這把鎖,使得沒有線程能夠start或者exit,這里釋放了之后就可以完成這些操作了;

執行線程安全點同步工作:SafepointSynchronize::block


上文已經了解了VMThread是如何進入和退出安全點的,但是還缺少一個環節,那就是讓線程阻塞在安全點位置上,SafepointSynchronize::block函數用來完成這部分工作,上文說到,線程進入安全點前會存在不同的執行狀態,當他們得知需要進入安全點后,就會調用block函數來阻塞住自己,直到安全點代碼完成執行,下面就來看看block函數的實現細節:

void SafepointSynchronize::block(JavaThread *thread) {
?
  // 1
  assert(thread != NULL, "thread must be set");
  assert(thread->is_Java_thread(), "not a Java thread");
?
  // 2
  JavaThreadState state = thread->thread_state();
?
  // 3
  // Check that we have a valid thread_state at this point
  switch(state) {
    case _thread_in_vm_trans:
    case _thread_in_Java:        // From compiled code
?
      // 4
      // We are highly likely to block on the Safepoint_lock. In order to avoid blocking in this case,
      // we pretend we are still in the VM.
      thread->set_thread_state(_thread_in_vm);
?
      // 5
      // We will always be holding the Safepoint_lock when we are examine the state
      // of a thread. Hence, the instructions between the Safepoint_lock->lock() and
      // Safepoint_lock->unlock() are happening atomic with regards to the safepoint code
      Safepoint_lock->lock_without_safepoint_check();
?
      // 6
      if (is_synchronizing()) {
?
        // 7
        // Decrement the number of threads to wait for and signal vm thread
        assert(_waiting_to_block > 0, "sanity check");
?
        // 8
        _waiting_to_block--;
?
        // 9
        thread->safepoint_state()->set_has_called_back(true);
?
        // 10
        if (thread->in_critical()) {
          // Notice that this thread is in a critical section
          increment_jni_active_count();
        }
?
        // 11
        // Consider (_waiting_to_block < 2) to pipeline the wakeup of the VM thread
        if (_waiting_to_block == 0) {
          Safepoint_lock->notify_all();
        }
      }
?
      // 12
      // We transition the thread to state _thread_blocked here, but
      // we can't do our usual check for external suspension and then
      // self-suspend after the lock_without_safepoint_check() call
      // below because we are often called during transitions while
      // we hold different locks. That would leave us suspended while
      // holding a resource which results in deadlocks.
      thread->set_thread_state(_thread_blocked);
?
?
      // 13
      Safepoint_lock->unlock();
?
      // 14
      // We now try to acquire the threads lock. Since this lock is hold by the VM thread during
      // the entire safepoint, the threads will all line up here during the safepoint.
      Threads_lock->lock_without_safepoint_check();
?
      // 15
      // restore original state. This is important if the thread comes from compiled code, so it
      // will continue to execute with the _thread_in_Java state.
      thread->set_thread_state(state);
?
      // 16
      Threads_lock->unlock();
      break;
?
    case _thread_in_native_trans:
    case _thread_blocked_trans:
    case _thread_new_trans:
?
      // 17
      // We transition the thread to state _thread_blocked here, but
      // we can't do our usual check for external suspension and then
      // self-suspend after the lock_without_safepoint_check() call
      // below because we are often called during transitions while
      // we hold different locks. That would leave us suspended while
      // holding a resource which results in deadlocks.
      thread->set_thread_state(_thread_blocked);
?
      // It is not safe to suspend a thread if we discover it is in _thread_in_native_trans. Hence,
      // the safepoint code might still be waiting for it to block. We need to change the state here,
      // so it can see that it is at a safepoint.
?
      // 18
      // Block until the safepoint operation is completed.
      Threads_lock->lock_without_safepoint_check();
?
      // 19
      // Restore state
      thread->set_thread_state(state);
?
      // 20
      Threads_lock->unlock();
      break;
  }
}
  • (1)當前線程肯定要是java線程,而不能是VMThread;

  • (2)獲取當前線程的狀態;

  • (3)看看當前線程處于什么狀態,不同狀態下處理的方式可能存在差異;

  • (4)狀態變更(需要研究)

  • (5)獲取Safepoint鎖,這個鎖VMThread在執行進入安全點的時候會持有,在迭代之后會稍微釋放一下(wait函數),這個時候其他java線程就能獲取到這個鎖,然后執行進入同步的代碼;

  • (6)判斷是否依然處于同步中狀態;

  • (7)_waitint_to_blcok計數器應該大于0,此時;

  • (8)更新這個計數器;

  • (9)這是為了讓VMThread知道當前函數以及調用block函數,進入安全點阻塞了;

  • (10)如果當前線程正在執行JNI代碼,則更新相關的計數器;

  • (11)如果當前線程是最后一個進入安全點的線程,那么就要通知VMThread線程繼續執行;

  • (12)標記線程進入block狀態;

  • (13)釋放Safepoint鎖,此時其他線程可以獲取到該鎖,執行這段代碼;

  • (14)獲取Threads_lock鎖,VMThread在進入安全點后會持有該鎖,所以,其他線程執行到這里就會被阻塞住,直到VMThread執行end函數退出安全點,這個線程才能獲取到該鎖,來退出安全點;

  • (15)讓線程狀態變更為進入安全點之前的狀態;

  • (16)釋放Threads_lock,其他線程才能從安全點退出;

  • (17)線程在狀態切換,則直接讓線程進入安全點;

  • (18、19、20)獲取到Threads_lock,進入阻塞,等待退出安全點;

讓線程進入安全點


上文提到,線程在進入安全點前,會有不同的狀態,下面來分析其中兩種狀態下線程是如何進入安全點的;

線程處于解釋執行字節碼狀態


首先,我們需要了解java其實是解釋+編譯結合起來的一門高性能語言,解釋執行的特點是啟動快,缺點就是允許慢,編譯的優點就是允許起來快,但是需要花費大量的時間來編譯成本地代碼,這里面涉及很多優化。

處于解釋字節碼執行狀態下,HotSpot使用一種稱為“模板解釋器”的技術來實現字節碼解釋執行,所謂“模板技術”,指的是每一個字節碼指令,都會被映射到字節碼解釋模板表中的一項模板,對應的是將指令翻譯成匯編代碼,這樣就能讓解釋執行的速度也可以很快。

了解了這一點,下面來開始看當線程處于執行字節碼解釋執行時是如何進入安全點的;在上文講到VMThread進入安全點的函數begin的時候,提到會執行一個函數:Interpreter::notice_safepoints,這個函數會通知模板解釋器,你需要在執行下一條字節碼的時候進入安全點:

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));
  }
}

這里將_safept_table拷貝到了_active_table中,那_active_table是一張什么表呢?字節碼模板解釋器在執行字節碼的時候,需要一張表來派發字節碼,如果需要線程進入安全點,那么就需要在執行字節碼前需要做一點額外的工作,下面來看看需要做什么額外的工作:

  { CodeletMark cm(_masm, "safepoint entry points");
    Interpreter::_safept_entry =
      EntryPoint(
                 generate_safept_entry_for(btos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(ztos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(ctos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(stos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(atos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(itos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(ltos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(ftos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(dtos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(vtos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint))
                 );
  }
    // installation of code in other places in the runtime
  // (ExcutableCodeManager calls not needed to copy the entries)
  set_safepoints_for_all_bytes();
 
void TemplateInterpreterGenerator::set_safepoints_for_all_bytes() {
  for (int i = 0; i < DispatchTable::length; i++) {
    Bytecodes::Code code = (Bytecodes::Code)i;
    if (Bytecodes::is_defined(code)) Interpreter::_safept_table.set_entry(code, Interpreter::_safept_entry);
  }
}

在初始化解釋器模板的時候會初始化這個table,可以看到傳入了一個函數:at_safepoint:

IRT_ENTRY(void, InterpreterRuntime::at_safepoint(JavaThread* thread))
  // We used to need an explict preserve_arguments here for invoke bytecodes. However,
  // stack traversal automatically takes care of preserving arguments for invoke, so
  // this is no longer needed.
?
  // IRT_END does an implicit safepoint check, hence we are guaranteed to block
  // if this is called during a safepoint
?
  if (JvmtiExport::should_post_single_step()) {
    // We are called during regular safepoints and when the VM is
    // single stepping. If any thread is marked for single stepping,
    // then we may have JVMTI work to do.
    JvmtiExport::at_single_stepping_point(thread, method(thread), bcp(thread));
  }
IRT_END

這個函數會在執行下一條字節碼的時候執行:

address TemplateInterpreterGenerator::generate_safept_entry_for (TosState state,
                                                                address runtime_entry) {
  address entry = __ pc();
  __ push(state);
  
  // 調用at_safepoint函數,用于進入安全點
  __ call_VM(noreg, runtime_entry);
  
  __ dispatch_via(vtos, Interpreter::_normal_table.table_for (vtos));
  return entry;
}

下面來看看at_safepoint是如何讓線程進入安全點的:

#define IRT_ENTRY(result_type, header)                               \
  result_type header {                                               \
    ThreadInVMfromJava __tiv(thread);                                \
    VM_ENTRY_BASE(result_type, header, thread)                       \
    debug_only(VMEntryWrapper __vew;)

IRT_ENTRY這個宏定義是關鍵,這個宏定義創建了一個ThreadInVMfromJava對象,創建這個對象的時候,會調用構造函數,當函數調用完成后,會自動調用析構函數,下面來看看這個類的構造函數和析構函數:

class ThreadInVMfromJava : public ThreadStateTransition {
 public:
  ThreadInVMfromJava(JavaThread* thread) : ThreadStateTransition(thread) {
    trans_from_java(_thread_in_vm);
  }
  ~ThreadInVMfromJava()  {
    if (_thread->stack_yellow_reserved_zone_disabled()) {
      _thread->enable_stack_yellow_reserved_zone();
    }
    trans(_thread_in_vm, _thread_in_Java);
    // Check for pending. async. exceptions or suspends.
    if (_thread->has_special_runtime_exit_condition()) _thread->handle_special_runtime_exit_condition();
  }
};
  • 在構造函數中,將線程的狀態變為了in_vm模式;

  • 在析構函數中,調用了trans函數,將線程狀態從in_vm變為了in_java

void trans(JavaThreadState from, JavaThreadState to)  {
   transition(_thread, from, to);
 }
?
  // Change threadstate in a manner, so safepoint can detect changes.
  // Time-critical: called on exit from every runtime routine
  static inline void transition(JavaThread *thread, JavaThreadState from, JavaThreadState to) {
?
    // 1
    assert(from != _thread_in_Java, "use transition_from_java");
    assert(from != _thread_in_native, "use transition_from_native");
?
    // 2
    // Change to transition state (assumes total store ordering!  -Urs)
    thread->set_thread_state((JavaThreadState)(from + 1));
?
    // 3
    if (SafepointSynchronize::do_call_back()) {
?
      // 4
      SafepointSynchronize::block(thread);
    }
?
    // 5
    thread->set_thread_state(to);
  }
  • (1)狀態校驗,避免調用錯誤;

  • (2)改變狀態;

  • (3)看看是否需要調用block進入安全點;

  • (4)調用block函數,進入安全點,阻塞直到VMThread完成安全點代碼執行并釋放Threads_lock;

  • (5)恢復線程狀態;

至此,處于字節碼解釋執行的線程是如何進入安全點的流程梳理清楚了,簡單來說,VMThread會在執行進入安全點代碼的begin函數的時候,將解釋器的字節碼路由表替換掉,然后在執行下一條字節碼之前插入檢查進入安全點的代碼,這樣,下一條字節碼解釋執行的時候就會進入安全點;

當然,有進入就有退出,VMThread完成代碼執行后,會執行end函數退出安全點,會調用ignore_safepoints函數,將字節碼派發表替換成原來的,這樣,執行下一條字節碼的時候,就不會插入進入安全點的代碼:

// 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));
    }
  }
}

線程處于運行編譯后代碼狀態


當線程正在執行已經被編譯成本地代碼的代碼的時候,會在一些位置讀取Safepoint——polling內存頁,VMThread在進入安全點的時候會將這個內存頁配置為不可讀,這樣,當線程試圖去讀這個內存頁的時候,就會產生錯誤信號,在linux下,錯誤信號處理器將會處理這個信號:

///////////////////////////////////////////////////////////////////////////////////
// signal handling (except suspend/resume)
?
// This routine may be used by user applications as a "hook" to catch signals.
// The user-defined signal handler must pass unrecognized signals to this
// routine, and if it returns true (non-zero), then the signal handler must
// return immediately.  If the flag "abort_if_unrecognized" is true, then this
// routine will never retun false (zero), but instead will execute a VM panic
// routine kill the process.
//
// If this routine returns false, it is OK to call it again.  This allows
// the user-defined signal handler to perform checks either before or after
// the VM performs its own checks.  Naturally, the user code would be making
// a serious error if it tried to handle an exception (such as a null check
// or breakpoint) that the VM was generating for its own correct operation.
//
// This routine may recognize any of the following kinds of signals:
//    SIGBUS, SIGSEGV, SIGILL, SIGFPE, SIGQUIT, SIGPIPE, SIGXFSZ, SIGUSR1.
// It should be consulted by handlers for any of those signals.
//
// The caller of this routine must pass in the three arguments supplied
// to the function referred to in the "sa_sigaction" (not the "sa_handler")
// field of the structure passed to sigaction().  This routine assumes that
// the sa_flags field passed to sigaction() includes SA_SIGINFO and SA_RESTART.
//
// Note that the VM will print warnings if it detects conflicting signal
// handlers, unless invoked with the option "-XX:+AllowUserSignalHandlers".
//
extern "C" JNIEXPORT int JVM_handle_linux_signal(int signo,
                                                 siginfo_t* siginfo,
                                                 void* ucontext,
                                                 int abort_if_unrecognized);

這個函數里面有一段和Safepoint相關的處理代碼:

        // Java thread running in Java code => find exception handler if any
        // a fault inside compiled code, the interpreter, or a stub
?
        if ((sig == SIGSEGV) && checkPollingPage(pc, (address)info->si_addr, &stub)) {
          break;
        }

在checkPollingPage函數內部會生成用于處理信號的函數,然后在后續執行這個函數,下面來看看checkPollingPage這個函數的實現細節:

inline static bool checkPollingPage(address pc, address fault, address* stub) {
  if (fault == os::get_polling_page()) {
    *stub = SharedRuntime::get_poll_stub(pc);
    return true;
  }
  return false;
}
?
address SharedRuntime::get_poll_stub(address pc) {
  address stub;
  // Look up the code blob
  CodeBlob *cb = CodeCache::find_blob(pc);
?
  bool at_poll_return = ((CompiledMethod*)cb)->is_at_poll_return(pc);
  bool has_wide_vectors = ((CompiledMethod*)cb)->has_wide_vectors();
  if (at_poll_return) {
?
    stub = SharedRuntime::polling_page_return_handler_blob()->entry_point();
  } else if (has_wide_vectors) {
?
    stub = SharedRuntime::polling_page_vectors_safepoint_handler_blob()->entry_point();
  } else {
?
    stub = SharedRuntime::polling_page_safepoint_handler_blob()->entry_point();
  }
?
  return stub;
}
?
?
  static SafepointBlob* polling_page_return_handler_blob()     { return _polling_page_return_handler_blob; }
  static SafepointBlob* polling_page_safepoint_handler_blob()  { return _polling_page_safepoint_handler_blob; }
  static SafepointBlob* polling_page_vectors_safepoint_handler_blob()  { return _polling_page_vectors_safepoint_handler_blob; }
?

這幾個blob在SharedRuntime::generate_stubs函數里面完成初始化:

//----------------------------generate_stubs-----------------------------------
void SharedRuntime::generate_stubs() {
  _wrong_method_blob                   = generate_resolve_blob(CAST_FROM_FN_PTR(address, SharedRuntime::handle_wrong_method),          "wrong_method_stub");
  _wrong_method_abstract_blob          = generate_resolve_blob(CAST_FROM_FN_PTR(address, SharedRuntime::handle_wrong_method_abstract), "wrong_method_abstract_stub");
  _ic_miss_blob                        = generate_resolve_blob(CAST_FROM_FN_PTR(address, SharedRuntime::handle_wrong_method_ic_miss),  "ic_miss_stub");
  _resolve_opt_virtual_call_blob       = generate_resolve_blob(CAST_FROM_FN_PTR(address, SharedRuntime::resolve_opt_virtual_call_C),   "resolve_opt_virtual_call");
  _resolve_virtual_call_blob           = generate_resolve_blob(CAST_FROM_FN_PTR(address, SharedRuntime::resolve_virtual_call_C),       "resolve_virtual_call");
  _resolve_static_call_blob            = generate_resolve_blob(CAST_FROM_FN_PTR(address, SharedRuntime::resolve_static_call_C),        "resolve_static_call");
  _resolve_static_call_entry           = _resolve_static_call_blob->entry_point();
?
#if defined(COMPILER2) || INCLUDE_JVMCI
  // Vectors are generated only by C2 and JVMCI.
  bool support_wide = is_wide_vector(MaxVectorSize);
  if (support_wide) {
    _polling_page_vectors_safepoint_handler_blob = generate_handler_blob(CAST_FROM_FN_PTR(address, SafepointSynchronize::handle_polling_page_exception), POLL_AT_VECTOR_LOOP);
  }
#endif // COMPILER2 || INCLUDE_JVMCI
  _polling_page_safepoint_handler_blob = generate_handler_blob(CAST_FROM_FN_PTR(address, SafepointSynchronize::handle_polling_page_exception), POLL_AT_LOOP);
  _polling_page_return_handler_blob    = generate_handler_blob(CAST_FROM_FN_PTR(address, SafepointSynchronize::handle_polling_page_exception), POLL_AT_RETURN);
?
  generate_deopt_blob();
?
#ifdef COMPILER2
  generate_uncommon_trap_blob();
#endif // COMPILER2
}

最后都會和handle_polling_page_exception函數有關:

void SafepointSynchronize::handle_polling_page_exception(JavaThread *thread) {
  assert(thread->is_Java_thread(), "polling reference encountered by VM thread");
  assert(thread->thread_state() == _thread_in_Java, "should come from Java code");
  assert(SafepointSynchronize::is_synchronizing(), "polling encountered outside safepoint synchronization");
?
  if (ShowSafepointMsgs) {
    tty->print("handle_polling_page_exception: ");
  }
?
  if (PrintSafepointStatistics) {
    inc_page_trap_count();
  }
?
  ThreadSafepointState* state = thread->safepoint_state();
?
  state->handle_polling_page_exception();
}

在state->handle_polling_page_exception函數中,會調用block函數進入安全點阻塞,直到VMThread退出安全點;

至此,就把安全點相關的內容大致學習完成了,安全點在HotSpot虛擬機中占有重要地位,其中GC就需要在安全點執行,通過本文的分析,可以學習到VMThread是如何讓所有線程停下來的,雖然簡單來說就是鎖柵欄,但是這其中還是有很多內容值得深入學習的。

image
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容