[Inside HotSpot] Java的方法調用

1. 方法調用模塊入口

Java全部的方法調用都會通過JavaCalls模塊。該模塊又細分爲call_virtual調用虛函數,call_static調用靜態函數等。虛函數調用會根據對象類型進行方法決議,因此須要獲取對象引用再查找實際要調用的方法;而靜態方法調用直接查找要調用的方法便可。無論怎樣,這些方法都是先找到要調用的方法methodHandle,而後傳給JavaCalls::call_helper()作實際的調用。html

2. 尋找調用方法

如今咱們知道了methodHandle表示實際要調用的方法,methodHandle裏面有一個指向當前線程的指針,還有一個指向Method類的指針,Method位於hotspot\share\oops\method.hpp,各類各樣的數據好比方法的訪問標誌,內聯標誌,用於編譯優化的計數等都落地於此。它的每一個屬性的意義都是肉眼可見的重要:java

_constMethod

指向方法中一些常量數據,好比常量池,max_local,max_stack,返回類型,參數個數,編譯-解釋適配器...這些參數的重要性不言而喻。編程

_method_data

存放一些計數信息和Profiling信息,好比方法重編譯了多少次,非逃逸參數有多少個,回邊有多少,有多少循環和基本塊。這些參數會影響後面的編譯器優化。app

_method_counters

大量編譯優化相關的計數:函數

  • 解釋器調用次數
  • 解釋執行時因爲異常而終止的次數
  • 方法調用次數(method裏面有多少方法調用)
  • 回邊個數
  • 該方法曾通過的分層編譯的最高層級
  • 熱點方法計數

_access_flag

flag 說明
ACC_PUBLIC 0x0001 方法是否爲public
ACC_PRIVATE 0x0002 方法是否爲private
ACC_PROTECTED 0x0004 方法是否爲protected
ACC_STATIC 0x0008 方法是否爲static
ACC_FINAL 0x0010 方法是否不可重寫
ACC_SYNCHRONIZED 0x0020 是否存在方法鎖
ACC_BRIDGE 0x0040 該方法是否由編譯器生成
ACC_VARARGS 0x0080 是否存在可變參數
ACC_NATIVE 0x0100 是否爲native方法
ACC_ABSTRACT 0x0400 是否爲抽象方法
ACC_STRICT 0x0800 是否啓用嚴格浮點模式
ACC_SYNTHETIC 0x1000 是不是源代碼裏面不存在的合成方法

_vtable_index

flag 說明
itable_index_max -10 首個itable索引
pending_itable_index -9 itable將會被賦值
invalid_vtable_index -4 無效虛表index
garbage_vtable_index -3 尚未初始化vtable的方法,垃圾值
nonvirtual_vtable_index -2 不須要虛函數派發,好比static函數就是這種

_flags

這個_flag不一樣於前面的_access_flag,它是表示這個方法具備什麼特徵,好比是否強制內聯,是否有@CallerSentitive註解,是不是有@HotSpotIntrinsicCandidate註解等oop

_intrinsic_id

固有方法(intrinsic method)在虛擬機中表示一些衆所周知的方法,針對它們能夠作特設處理,生成獨特的代碼例程,虛擬機發現一個方法是固有方法就不會走逐行解釋字節碼這條路徑而是跳到獨特的代碼例程上面,全部的固有方法都定義在hotspot\share\classfile\vmSymbols.hpp中,有興趣的能夠去看看。優化

_compiled_invocation_count

編譯後的方法叫nmethod,這個就是用來計數編譯後的nmethod調用了多少次,若是該方法是解釋執行就爲0。.net

_code

指向編譯後的本地代碼。線程

_from_interpreter_entry

解釋器入口,這個很是重要。以前提到JavaCalls::call獲得methodHandle傳給call_helper作實際調用,call_helper會使用這個入口進入解釋器的世界。指針

_from_compiled_entry

若是該方法已經通過了編譯,那麼就會使用該入口執行編譯後的代碼。

虛擬機是解釋編譯混合執行的模型,一個方法可能A時刻是解釋模式,B時刻是編譯模式,這就要求兩個入口都能進入正確的地方。hotspot使用一個適配器完成解釋編譯模式的切換:

之因此要加一個適配器是由於編譯產出的本地代碼用寄存器存放參數,解釋器用棧存放參數,適配器能夠消除這些不一樣,同時正確設置入口點。

3. 創建棧幀

前面說道找到methodHandle後傳給call_helper作調用。其實,嚴格來講,call_helper尚未作方法調用,它只是檢查了下方法是否須要進行編譯,驗證了參數等等,最終它是調用函數指針_call_stub_entry,把方法調用這件事又轉交給了_call_stub_entry。

// hotspot\share\runtime\javaCalls.cpp
void JavaCalls::call_helper(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
  ...
  // 調用函數指針_call_stub_entry,把實際的函數調用工做轉交給它。
  { JavaCallWrapper link(method, receiver, result, CHECK);
    { HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner
      StubRoutines::call_stub()(
        (address)&link,
        result_val_address,      
        result_type,
        method(),
        entry_point,
        args->parameters(),
        args->size_of_parameters(),
        CHECK
      );

      result = link.result();  
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
  } 
}

_call_stub_entry由generate_call_stub()生成,當調用Java方法前須要創建棧幀,該棧幀就是於此創建的。
另外StubRoutines::call_stub()()是將_call_stub_entry強制類型轉換爲指針而後執行,調試的時候不能對應源碼。若是使用Microsoft Visual Studio系列編譯器,點擊菜單欄調試->窗口->反彙編

而後在反彙編窗口STEP INTO進入call

在右方能夠看到generate_call_stub()生成的機器碼(的彙編表示)了。因爲generate_call_stub太多,這裏就不逐行對照,請自行對應源碼和反彙編窗口的輸出,generate_call_stub裏面是用匯編形式寫的機器碼生成,所有貼出來既無必要也沒意思,因此用註釋代替了,只保留最重要的邏輯:

// hotspot\cpu\x86\stubGenerator_x86_32.cpp
address generate_call_stub(address& return_address) {
    // 保存重要的參數好比解釋器入口點,Java方法返回地址等
    // 將Java方法的參數壓入棧

    // 調用Java方法
    __ movptr(rbx, method);           // 將Method*指針存放到rbx
    __ movptr(rax, entry_point);      // 將解釋器入口存放到rax
    __ mov(rsi, rsp);                 // 將當前棧頂存放到rsi
    __ call(rax);                     // 進入解釋器入口!

    // 處理Java方法返回值
    // 彈出Java參數
    // 返回
    return start;
  }

它首先創建了一個棧幀,這個棧幀裏面保存了一些重要的數據,再把Java方法的參數壓入棧,當這一步完成,棧幀變成了這個樣子:

4. Java方法調用

當棧幀創建完畢就能夠調用Java方法了。重複一次,Java方法調用使用以下代碼:

// 調用Java方法
    __ movptr(rbx, method);           // 將Method*指針存放到rbx
    __ movptr(rax, entry_point);      // 將解釋器入口存放到rax
    __ mov(rsi, rsp);                 // 將當前棧頂存放到rsi
    __ call(rax);                     // 進入解釋器入口!

前面三句將重要的數據放入寄存器,而後call rax至關於call entry_point,這個entry_point即解釋器入口點,最終的方法執行過程實際上是在這裏面的,_call_stub_entry只是一個樁代碼(Stub code),建立了棧幀,處理調用返回,實際的調用仍是要跳到解釋器裏面的。

樁代碼的意義有不少,常見的就是它是一個符合要求的簽名的函數,可是函數如今尚未徹底實現,那就留一個樁佔位。好比一個系統須要讀取外部溫度:

void work(){
  float temperature = readTemperatureFromSensor();
  if(temperature>40.0){
    ...
  }
}
float readTemperatureFromSensor(){
  return 42.0f;
}

這個讀溫度的函數比較複雜,涉及傳感器的硬件編程,現階段咱們只想完成外部即work的邏輯,那麼就將readTemperatureFromSensor()作爲一個stub,寫一個假的實現,後面再補全。

回到主題,虛擬機_call_stub_entry樁代碼的意思是它不完成具體任務(方法調用),只是作一些輔助工做(創建棧幀),而是跳到(call rax)解釋器入口完成具體任務,虛擬機中還有不少這樣的模式,其它叫法還有trampoline(跳牀),之後都會遇到。

5. 總結

學而不思則罔,思而不學則殆。咱們大概清楚了Java方法調用的流程,如今能夠試着來總結一下:
JavaCalls裏面的call_static()或者call_virtual經過方法決議找到要調用的方法methodHandle,傳遞給JavaCalls::call();JavaCalls::call()作一些簡單的檢查,好比方法是否須要進行C1/C2 JIT,參數對不對,以後調用_call_stub_entry,它會創建棧幀,進入解釋器執行字節碼,最後從解釋器返回,處理返回值,完成方法調用。詳細的調用棧以下:

JavaCalls::call_static()          // 找到要調用的方法
  -> JavaCalls::call()               
    -> os::os_exception_wrapper()
      -> JavaCalls::call_helper()
        -> _call_stub_entry()     // 創建棧幀,處理解釋器返回值
          -> `call rbx`           // 進入解釋器入口點

附錄1. 使用hsdis查看對應的彙編表示

若是以爲上述調試方法過於麻煩,還有備選方案。下載hsdis-amd64.dll,將它放在jdk/bin/server/目錄下,而後虛擬機加上參數-XX:+UnlockDiagnosticVMOptions -XX:+PrintStubCode還能夠查看上面的生成的機器代碼的彙編形式,不過除了驗證比對外通常人也很難從這大段彙編中看出什麼...:

StubRoutines::call_stub [0x000001a53eb80b0c, 0x000001a53eb80efe[ (1010 bytes)
  0x000001a53eb80b0c: push   %rbp
  0x000001a53eb80b0d: mov    %rsp,%rbp
  0x000001a53eb80b10: sub    $0x1d8,%rsp
  0x000001a53eb80b17: mov    %r9,0x28(%rbp)
  0x000001a53eb80b1b: mov    %r8d,0x20(%rbp)
  0x000001a53eb80b1f: mov    %rdx,0x18(%rbp)
  0x000001a53eb80b23: mov    %rcx,0x10(%rbp)
  0x000001a53eb80b27: mov    %rbx,-0x8(%rbp)
  0x000001a53eb80b2b: mov    %r12,-0x20(%rbp)
  0x000001a53eb80b2f: mov    %r13,-0x28(%rbp)
  0x000001a53eb80b33: mov    %r14,-0x30(%rbp)
  0x000001a53eb80b37: mov    %r15,-0x38(%rbp)
  0x000001a53eb80b3b: vmovdqu %xmm6,-0x48(%rbp)
  0x000001a53eb80b40: vmovdqu %xmm7,-0x58(%rbp)
  0x000001a53eb80b45: vmovdqu %xmm8,-0x68(%rbp)
  0x000001a53eb80b4a: vmovdqu %xmm9,-0x78(%rbp)
  0x000001a53eb80b4f: vmovdqu %xmm10,-0x88(%rbp)
  0x000001a53eb80b57: vmovdqu %xmm11,-0x98(%rbp)
  0x000001a53eb80b5f: vmovdqu %xmm12,-0xa8(%rbp)
  0x000001a53eb80b67: vmovdqu %xmm13,-0xb8(%rbp)
  0x000001a53eb80b6f: vmovdqu %xmm14,-0xc8(%rbp)
  0x000001a53eb80b77: vmovdqu %xmm15,-0xd8(%rbp)
 ; 省略500+行

附錄2. 解釋器入口點

意猶未盡嗎?上面省略了不少東西,好比進入解釋器入口點執行字節碼這個重要的事情。那麼解釋器入口點在哪?咱們知道解釋器是在虛擬機建立的時候JIT生成的,能夠跟蹤虛擬機建立找到它,它的調用棧以下:

Threads::create_vm()
  -> init_globals() 
    -> interpreter_init()()
      -> TemplateInterpreter::initialize()
        -> TemplateInterpreterGenerator()   // 構造函數
          -> TemplateInterpreterGenerator::generate_all()
            -> TemplateInterpreterGenerator::generate_normal_entry()

普通方法(非synchronized,非native)的解釋器入口點是經過\hotspot\cpu\x86\templateInterpreterGenerator_x86.cpp中的generate_normal_entry()生成的。

附錄3. 設置解釋器入口點

仍是這個問題,咱們知道了解釋器入口點在哪,可是這個解釋器入口點又是怎麼和方法關聯起來的呢?

Java的類在虛擬機中會通過加載 -> 連接 -> 初始化 三個步驟,網上有不少詳細解釋這裏就不在贅述。具體來講instanceKlass在虛擬機中表示一個Java類,它使用instanceKlass::link_class()作連接過程。類的連接會觸發類中方法的Method::link_method(),它會給方法設置正確的解釋器入口點,編譯器適配器等:

// hotspot\share\oops\method.cpp
void Method::link_method(const methodHandle& h_method, TRAPS) {
  ... 
  if (!is_shared()) {
    // entry_for_method會找到剛剛generate_normal_entry設置的入口點
    address entry = Interpreter::entry_for_method(h_method);
    // 將它設置爲解釋器入口點,便可_i2i_entry和_from_interpreted_entry
    set_interpreter_entry(entry);
  }
  ...
  // 設置_from_compiled_entry的適配器
  (void) make_adapters(h_method, CHECK);
}
相關文章
相關標籤/搜索