為什麼方法斷點那麼慢

原文
一些IDE提供「方法斷點」的功能,可以讓斷點調試看起來非常簡潔,然而在調試過程中我們會發現調試反應時間很長,調試器的性能大大降低。在本文中,我會簡單解釋方法斷點的實現原理,以及為何導致性能變差的原因。

為了更好的理解,我先簡單說明一下斷點是如何實現的,以及調試器的工作原理。

JPDA(Java Platform Debugger Architecture)

JPDA是JAVA調試框架,主要用於debugger(調試器)和debuggee(調試程式或進程)之間的通訊。JPDA主要由三個主要API構成。

  1. JVM TI(JVM Tool Interface) : 一個native介面,定義了VM提供debug的函數。
  2. Java Debug Wire Protocol(JDWP):JDWP是一個定義debugger和debuggee通訊的Api。
  3. Java Debug Interface(JDI): Java介面,用於前端和後端的通訊交互,JDI內部實現了JDWP介面。
  4. 下圖和文章中的前後端(back-end and font-end)分別指的是運行在VM上的調試程式(進程)和編輯器。
  5. 調試鏈:相關事件發生時(比如打斷點,單步調試,調試時修改參數值),VM通過回調(JNI: java Native Interface,VM通過JNI來調用Native Interface)調用JVM TI,然後back-end發送event給font-end。debugger通過JDI和JDWP與後端通訊。

為何要用方法斷點

如果調用的方法無法訪問源碼,或者方法內有多個if出口,此時用方法斷點很簡潔。

JAVA斷點原理

在編輯器打一個斷點,往往內部會進行三步

  1. 允許斷點事件:VM允許debugger激活各種事件。font-end調用 SetEventNotificationMode() 方法啟用 can_generate_breakpoint_events ,當運行到斷點處,VM會觸發事件通過debugger鏈返回值。
  2. 註冊斷點:通過 SetBreakpoint 方法設置斷點,當執行緒運行到斷點處,VM會將所有active執行緒暫停,並且觸發斷點事件。
    SetBreakpoint(jvmtiEnv* env,
    
                  jmethodID method, //注意一下此變數,下文會再次提到。
    
                  jlocation location)
    
  3. 斷點事件:VM觸發的事件叫斷點事件,用於通知debugger。事件: Breakpoint(xxx)

方法斷點

實際上JDPA不提供方法斷點的功能,方法斷點是編輯器提供的。

debugger調用上文說的 SetEventNotificationMode()
啟用 can_generate_method_entry_eventscan_generate_method_exit_events,當VM運行進入和退出方法時,會向debugger發送 方法進入退出事件:

MethodEntry(....,JmethodID method)
MethodExit(....,JmethodID method)

斷點實現流程:

  1. IDE將斷點添加到編輯器內置維護的一個斷點list里。
  2. debugger調用上文說的SetEventNotificationMode(),啟用entry method和exit method,當VM運行程式碼進入和退出方法時,會向debugger發送事件。
  3. 每當進入和退出方法時,VM會向font-end發送MethodEntry或MethodExit。
  4. IDE根據事件中的jmethodID,來檢索該id是否存在於斷點list中。
  5. 如果存在,debugger則調用 SetBreakPoint 方法,將請求發送到VM。
  6. VM運行程式碼到斷點處,停止活動執行緒,並且將event返回debugger

和普通斷點的區別在於:方法斷點在流程中需要先判斷該方法是否被前端標記為應該要打上斷點,然後才是註冊斷點。

調試方法斷點為何很慢

  1. JmethodID:JmethodID是正在運行方法的標識符。每次VM需要返回MethodEntry和MethodExit時都需要攜帶JmethodID,然而VM查找獲取JmethodID需要較長時間。
  2. communication:methodEntry和MethodExit導致前端和後端之間進行大量的通訊往返。
  3. VM callback is synchronization:VM觸發事件使用回調時,經過以下幾個步驟(都是同步操作):
    1. 將context切換到back-end,back-end通知font-end
    2. font-end根據返回的jmethodID,查找是否存在於斷點list中。
      在此期間程式碼執行是暫停的。

總結

  1. 盡量減少方法斷點的使用。
  2. 如果不必要,可以只使用methodEntry,不激活methodExit,減少查找以及通訊次數。