第2篇-JVM虛擬機這樣來調用Java主類的main()方法

  • 2021 年 8 月 12 日
  • 筆記

在前一篇 第1篇-關於JVM運行時,開篇說的簡單些 中介紹了call_static()、call_virtual()等函數的作用,這些函數會調用JavaCalls::call()函數。我們看Java類中main()方法的調用,調用棧如下:

JavaCalls::call_helper() at javaCalls.cpp	
os::os_exception_wrapper() at os_linux.cpp	
JavaCalls::call() at javaCalls.cpp
jni_invoke_static() at jni.cpp	
jni_CallStaticVoidMethod() at jni.cpp	
JavaMain() at java.c
start_thread() at pthread_create.c
clone() at clone.S

這是Linux上的調用棧,通過JavaCalls::call_helper()函數來執行main()方法。棧的起始函數為clone(),這個函數會為每個進程(Linux進程對應着Java線程)創建單獨的棧空間,這個棧空間如下圖所示。

 

在Linux操作系統上,棧的地址向低地址延伸,所以未使用的棧空間在已使用的棧空間之下。圖中的每個藍色小格表示對應方法的棧幀,而棧就是由一個一個的棧幀組成。native方法的棧幀、Java解釋棧幀和Java編譯棧幀都會在黃色區域中分配,所以說他們寄生在宿主棧中,這些不同的棧幀都緊密的挨在一起,所以並不會產生什麼空間碎片這類的問題,而且這樣的布局非常有利於進行棧的遍歷。上面給出的調用棧就是通過遍歷一個一個棧幀得到的,遍歷過程也是棧展開的過程。後續對於異常的處理、運行jstack打印線程堆棧、GC查找根引用等都會對棧進行展開操作,所以棧展開是後面必須要介紹的。

下面我們繼續看JavaCalls::call_helper()函數,這個函數中有個非常重要的調用,如下:

// do call
{
    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();  // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
      // Preserve oop return value across possible gc points
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)

調用StubRoutines::call_stub()函數返回一個函數指針,然後通過函數指針來調用函數指針指向的函數。通過函數指針調用和通過函數名調用的方式一樣,這裡我們需要清楚的是,調用的目標函數仍然是C/C++函數,所以由C/C++函數調用另外一個C/C++函數時,要遵守調用約定。這個調用約定會規定怎麼給被調用函數(Callee)傳遞參數,以及被調用函數的返回值將存儲在什麼地方。

下面我們就來簡單說說Linux X86架構下的C/C++函數調用約定,在這個約定下,以下寄存器用於傳遞參數: 

  • 第1個參數:rdi    c_rarg0   
  • 第2個參數:rsi    c_rarg1   
  • 第3個參數:rdx   c_rarg2  
  • 第4個參數:rcx   c_rarg3  
  • 第5個參數:r8     c_rarg4  
  • 第6個參數:r9     c_rarg5  

在函數調用時,6個及小於6個用如下寄存器來傳遞,在HotSpot中通過更易理解的別名c_rarg*來使用對應的寄存器。如果參數超過六個,那麼程序將會用調用棧來傳遞那些額外的參數。

數一下我們通過函數指針調用時傳遞了幾個參數?8個,那麼後面的2個就需要通過調用函數(Caller)的棧來傳遞,這兩個參數就是args->size_of_parameters()和CHECK(這是個宏,擴展後就是傳遞線程對象)。

所以我們的調用棧在調用函數指針指向的函數時,變為了如下狀態:

 

右邊是具體的call_helper()棧幀中的內容,我們把thread和parameter size壓入了調用棧中,其實在調目標函數的過程還會開闢新的棧幀並在parameter size後壓入返回地址和調用棧的棧底,下一篇我們再詳細介紹。先來介紹下JavaCalls::call_helper()函數的實現,我們分3部分依次介紹。

1、檢查目標方法是否”首次執行前就必須被編譯」,是的話調用JIT編譯器去編譯目標方法;

代碼實現如下:

void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
  methodHandle method = *m;
  JavaThread* thread = (JavaThread*)THREAD;
  ...
 
  assert(!thread->is_Compiler_thread(), "cannot compile from the compiler");
  if (CompilationPolicy::must_be_compiled(method)) {
    CompileBroker::compile_method(method, InvocationEntryBci,
                                  CompilationPolicy::policy()->initial_compile_level(),
                                  methodHandle(), 0, "must_be_compiled", CHECK);
  }
  ...
}

對於main()方法來說,如果配置了-Xint選項,則是以解釋模式執行的,所以並不會走上面的compile_method()函數的邏輯。後續我們要研究編譯執行時,可以強制要求進行編譯執行,然後查看執行過程。

2、獲取目標方法的解釋模式入口from_interpreted_entry,也就是entry_point的值。獲取的entry_point就是為Java方法調用準備棧楨,並把代碼調用指針指向method的第一個位元組碼的內存地址。entry_point相當於是method的封裝,不同的method類型有不同的entry_point。

接着看call_helper()函數的代碼實現,如下:

address entry_point = method->from_interpreted_entry();

調用method的from_interpreted_entry()函數獲取Method實例中_from_interpreted_entry屬性的值,這個值到底在哪裡設置的呢?我們後面會詳細介紹。

3、調用call_stub()函數,需要傳遞8個參數。這個代碼在前面給出過,這裡不再給出。下面我們詳細介紹一下這幾個參數,如下: 

(1)link 此變量的類型為JavaCallWrapper,這個變量對於棧展開過程非常重要,後面會詳細介紹;

(2)result_val_address 函數返回值地址;

(3)result_type 函數返回類型;

(4)method() 當前要執行的方法。通過此參數可以獲取到Java方法所有的元數據信息,包括最重要的位元組碼信息,這樣就可以根據位元組碼信息解釋執行這個方法了;

(5)entry_point HotSpot每次在調用Java函數時,必然會調用CallStub函數指針,這個函數指針的值取自_call_stub_entry,HotSpot通過_call_stub_entry指向被調用函數地址。在調用函數之前,必須要先經過entry_point,HotSpot實際是通過entry_point從method()對象上拿到Java方法對應的第1個位元組碼命令,這也是整個函數的調用入口;

(6)args->parameters()  描述Java函數的入參信息;

(7)args->size_of_parameters()  描述Java函數的入參的大小;

(8)CHECK 當前線程對象。  

這裡最重要的就是entry_point了,這也是下一篇要介紹的內容。

搭建過程中如果有問題可直接評論留言或加作者微信mazhimazh。

關注公眾號,有HotSpot源碼剖析系列文章!