【超硬核】JVM原始碼解讀:Java方法main在虛擬機器上解釋執行

HeapDump效能社群發表於2021-11-11
本文由HeapDump效能社群首席講師鳩摩(馬智)授權整理髮布

第1篇-關於Java虛擬機器HotSpot,開篇說的簡單點

開講Java執行時,這一篇講一些簡單的內容。我們寫的主類中的main()方法是如何被Java虛擬機器呼叫到的?在Java類中的一些方法會被由C/C++編寫的HotSpot虛擬機器的C/C++函式呼叫,不過由於Java方法與C/C++函式的呼叫約定不同,所以並不能直接呼叫,需要JavaCalls::call()這個函式輔助呼叫。(我把由C/C++編寫的叫函式,把Java編寫的叫方法,後續也會延用這樣的叫法)如下圖所示。

從C/C++函式中呼叫的一些Java方法主要有:

(1)Java主類中的main()方法;

(2)Java主類裝載時,呼叫JavaCalls::call()函式執行checkAndLoadMain()方法;

(3)類的初始化過程中,呼叫JavaCalls::call()函式執行的Java類初始化方法,可以檢視JavaCalls::call_default_constructor()函式,有對方法的呼叫邏輯;

(4)我們先省略main方法的執行流程(其實main方法的執行也是先啟動一個JavaMain執行緒,套路都是一樣的),單看某個JavaThread的啟動過程。JavaThread的啟動最終都要通過一個native方法java.lang.Thread#start0()方法完成的,這個方法經過直譯器的native_entry入口,呼叫到了JVM_StartThread()函式。其中的static void thread_entry(JavaThread* thread, TRAPS)函式中會呼叫JavaCalls::call_virtual()函式。JavaThread最終會通過JavaCalls::call_virtual()函式來呼叫位元組碼中的run()方法;

(5)在SystemDictionary::load_instance_class()這個能體現雙親委派的函式中,如果類載入器物件不為空,則會呼叫這個類載入器的loadClass()函式(通過call_virtual()函式來呼叫)來載入類。

當然還會有其它方法,這裡就不一一列舉了。通過JavaCalls::call()、JavaCalls::call_helper()等函式呼叫Java方法,這些函式定義在JavaCalls類中,這個類的定義如下:

從C/C++函式中呼叫的一些Java方法主要有:

(1)Java主類中的main()方法;

(2)Java主類裝載時,呼叫JavaCalls::call()函式執行checkAndLoadMain()方法;

(3)類的初始化過程中,呼叫JavaCalls::call()函式執行的Java類初始化方法,可以檢視JavaCalls::call_default_constructor()函式,有對方法的呼叫邏輯;

(4)我們先省略main方法的執行流程(其實main方法的執行也是先啟動一個JavaMain執行緒,套路都是一樣的),單看某個JavaThread的啟動過程。JavaThread的啟動最終都要通過一個native方法java.lang.Thread#start0()方法完成的,這個方法經過直譯器的native_entry入口,呼叫到了JVM_StartThread()函式。其中的static void thread_entry(JavaThread* thread, TRAPS)函式中會呼叫JavaCalls::call_virtual()函式。JavaThread最終會通過JavaCalls::call_virtual()函式來呼叫位元組碼中的run()方法;

(5)在SystemDictionary::load_instance_class()這個能體現雙親委派的函式中,如果類載入器物件不為空,則會呼叫這個類載入器的loadClass()函式(通過call_virtual()函式來呼叫)來載入類。

當然還會有其它方法,這裡就不一一列舉了。通過JavaCalls::call()、JavaCalls::call_helper()等函式呼叫Java方法,這些函式定義在JavaCalls類中,這個類的定義如下:

原始碼位置:openjdk/hotspot/src/share/vm/runtime/javaCalls.hpp

class JavaCalls: AllStatic {
  static void call_helper(JavaValue* result, methodHandle* method, JavaCallArguments* args, TRAPS);
 public:

  static void call_default_constructor(JavaThread* thread, methodHandle method, Handle receiver, TRAPS);

  // 使用如下函式呼叫Java中一些特殊的方法,如類初始化方法<clinit>等
  // receiver表示方法的接收者,如A.main()呼叫中,A就是方法的接收者
  static void call_special(JavaValue* result, KlassHandle klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS); 
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);

  // 使用如下函式呼叫動態分派的一些方法
  static void call_virtual(JavaValue* result, KlassHandle spec_klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, TRAPS); 
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);

  // 使用如下函式呼叫Java靜態方法
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, JavaCallArguments* args, TRAPS);
   static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS);
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
  static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);

  // 更低一層的介面,如上的一些函式可能會最終呼叫到如下這個函式
  static void call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS);
};

如上的函式都是自解釋的,通過名稱我們就能看出這些函式的作用。其中JavaCalls::call()函式是更低一層的通用介面。Java虛擬機器規範定義的位元組碼指令共有5個,分別為invokestatic、invokedynamic、invokestatic、invokespecial、invokevirtual幾種方法呼叫指令。這些call_static()、call_virtual()函式內部呼叫了call()函式。這一節我們先不介紹各個方法的具體實現。下一篇將詳細介紹。  

我們選一個重要的main()方法來檢視具體的呼叫邏輯。如下基本照搬R大的內容,不過我略做了一些修改,如下:

假設我們的Java主類的類名為JavaMainClass,下面為了區分java launcher裡C/C++的main()與Java層程式裡的main(),把後者寫作JavaMainClass.main()方法。
從剛進入C/C++的main()函式開始:

啟動並呼叫HotSpot虛擬機器的main()函式的執行緒執行的主要邏輯如下:

main()
-> //... 做一些引數檢查
-> //... 開啟新執行緒作為main執行緒,讓它從JavaMain()函式開始執行;該執行緒等待main執行緒執行結束

在如上執行緒中會啟動另外一個執行緒執行JavaMain()函式,如下:

JavaMain()
-> //... 找到指定的JVM
-> //... 載入並初始化JVM
-> //... 根據Main-Class指定的類名載入JavaMainClass
-> //... 在JavaMainClass類裡找到名為"main"的方法,簽名為"([Ljava/lang/String;)V",修飾符是public的靜態方法
-> (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // 通過JNI呼叫JavaMainClass.main()方法

以上步驟都還在java launcher的控制下;當控制權轉移到JavaMainClass.main()方法之後就沒java launcher什麼事了,等JavaMainClass.main()方法返回之後java launcher才接手過來清理和關閉JVM。

下面看一下呼叫Java主類main()方法時會經過的主要方法及執行的主要邏輯,如下:

// HotSpot VM裡對JNI的CallStaticVoidMethod的實現。留意要傳給Java方法的引數
// 以C的可變長度引數傳入,這個函式將其收集打包為JNI_ArgumentPusherVaArg物件
-> jni_CallStaticVoidMethod()

     // 這裡進一步將要傳給Java的引數轉換為JavaCallArguments物件傳下去    
     -> jni_invoke_static()

        // 真正底層實現的開始。這個方法只是層皮,把JavaCalls::call_helper()
        // 用os::os_exception_wrapper()包裝起來,目的是設定HotSpot VM的C++層面的異常處理
        -> JavaCalls::call()   

           -> JavaCalls::call_helper()
              -> //... 檢查目標方法是否為空方法,是的話直接返回
              -> //... 檢查目標方法是否“首次執行前就必須被編譯”,是的話呼叫JIT編譯器去編譯目標方法
              -> //... 獲取目標方法的解釋模式入口from_interpreted_entry,下面將其稱為entry_point
              -> //... 確保Java棧溢位檢查機制正確啟動
              -> //... 建立一個JavaCallWrapper,用於管理JNIHandleBlock的分配與釋放,
                 // 以及在呼叫Java方法前後儲存和恢復Java的frame pointer/stack pointer

              //... StubRoutines::call_stub()返回一個指向call stub的函式指標,
              // 緊接著呼叫這個call stub,傳入前面獲取的entry_point和要傳給Java方法的引數等資訊
              -> StubRoutines::call_stub()(...) 
                 // call stub是在VM初始化時生成的。對應的程式碼在
                 // StubGenerator::generate_call_stub()函式中
                 -> //... 把相關暫存器的狀態調整到直譯器所需的狀態
                 -> //... 把要傳給Java方法的引數從JavaCallArguments物件解包展開到解釋模
                    // 式calling convention所要求的位置
                 -> //... 跳轉到前面傳入的entry_point,也就是目標方法的from_interpreted_entry

                    -> //... 在-Xcomp模式下,實際跳入的是i2c adapter stub,將解釋模式calling convention
                       // 傳入的引數挪到編譯模式calling convention所要求的位置
                           -> //... 跳轉到目標方法被JIT編譯後的程式碼裡,也就是跳到 nmethod 的 VEP 所指向的位置
                                -> //... 正式開始執行目標方法被JIT編譯好的程式碼 <- 這裡就是"main()方法的真正入口"

後面3個步驟是在編譯執行的模式下,不過後續我們從解釋執行開始研究,所以需要為虛擬機器配置-Xint選項,有了這個選項後,Java主類的main()方法就會解釋執行了。

在呼叫Java主類main()方法的過程中,我們看到了虛擬機器是通過JavaCalls::call()函式來間接呼叫main()方法的,下一篇我們研究一下具體的呼叫邏輯。

第2篇-Java虛擬機器這樣來呼叫Java主類的main()方法

在前一篇第1篇-關於Java虛擬機器HotSpot,開篇說的簡單些 中介紹了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();  
      // Preserve oop return value across possible gc points
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
}

呼叫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() 引數需要佔用的,以字為單位的記憶體大小

(8)CHECK 當前執行緒物件。

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

第3篇-CallStub新棧幀的建立

在前一篇文章第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法中我們介紹了在call_helper()函式中通過函式指標的方式呼叫了一個函式,如下:

StubRoutines::call_stub()(
         (address)&link,
         result_val_address,              
         result_type,
         method(),
         entry_point,
         args->parameters(),
         args->size_of_parameters(),
         CHECK
);

其中呼叫StubRoutines::call_stub()函式會返回一個函式指標,查清楚這個函式指標指向的函式的實現是我們這一篇的重點。 呼叫的call_stub()函式的實現如下:

原始碼位置:openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp

static CallStub  call_stub() { 
    return CAST_TO_FN_PTR(CallStub, _call_stub_entry); 
}

call_stub()函式返回一個函式指標,指向依賴於作業系統和cpu架構的特定的方法,原因很簡單,要執行native程式碼,得看看是什麼cpu架構以便確定暫存器,看看什麼os以便確定ABI。

其中CAST_TO_FN_PTR是巨集,具體定義如下:

原始碼位置:/src/share/vm/runtime/utilities/globalDefinitions.hpp

#define CAST_TO_FN_PTR(func_type, value) ((func_type)(castable_address(value)))

對call_stub()函式進行巨集替換和展開後會變為如下的形式:

static CallStub call_stub(){
    return (CallStub)( castable_address(_call_stub_entry) );
}

CallStub的定義如下:

原始碼位置:openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp

typedef void (*CallStub)(
    // 聯結器
    address   link,    
    // 函式返回值地址
    intptr_t* result, 
    //函式返回型別 
    BasicType result_type, 
    // JVM內部所表示的Java方法物件
    Method*   method, 
    // JVM呼叫Java方法的例程入口。JVM內部的每一段
    // 例程都是在JVM啟動過程中預先生成好的一段機器指令。
    // 要呼叫Java方法, 必須經過本例程, 
    // 即需要先執行這段機器指令,然後才能跳轉到Java方法
    // 位元組碼所對應的機器指令去執行
    address   entry_point, 
    intptr_t* parameters,
    int       size_of_parameters,
    TRAPS
);

如上定義了一種函式指標型別,指向的函式宣告瞭8個形式引數。 

在call_stub()函式中呼叫的castable_address()函式定義在globalDefinitions.hpp檔案中,具體實現如下:

inline address_word  castable_address(address x)  { 
    return address_word(x) ; 
}

address_word是一定自定義的型別,在globalDefinitions.hpp檔案中的定義如下:

typedef   uintptr_t     address_word;

其中uintptr_t也是一種自定義的型別,在Linux核心的作業系統下使用globalDefinitions_gcc.hpp檔案中的定義,具體定義如下:

typedef  unsigned int  uintptr_t;

這樣call_stub()函式其實等同於如下的實現形式:

static CallStub call_stub(){
    return (CallStub)( unsigned int(_call_stub_entry) );
}

將_call_stub_entry強制轉換為unsigned int型別,然後以強制轉換為CallStub型別。CallStub是一個函式指標,所以_call_stub_entry應該也是一個函式指標,而不應該是一個普通的無符號整數。

在call_stub()函式中,_call_stub_entry的定義如下:

address StubRoutines::_call_stub_entry = NULL;

_call_stub_entry的初始化在在openjdk/hotspot/src/cpu/x86/vm/stubGenerator_x86_64.cpp檔案下的generate_initial()函式,呼叫鏈如下:

StubGenerator::generate_initial()   stubGenerator_x86_64.cpp    
StubGenerator::StubGenerator()      stubGenerator_x86_64.cpp
StubGenerator_generate()            stubGenerator_x86_64.cpp    
StubRoutines::initialize1()         stubRoutines.cpp    
stubRoutines_init1()                stubRoutines.cpp    
init_globals()                      init.cpp
Threads::create_vm()                thread.cpp
JNI_CreateJavaVM()                  jni.cpp
InitializeJVM()                     java.c
JavaMain()                          java.c

其中的StubGenerator類定義在openjdk/hotspot/src/cpu/x86/vm目錄下的stubGenerator_x86_64.cpp檔案中,這個檔案中的generate_initial()方法會初始化call_stub_entry變數,如下:

StubRoutines::_call_stub_entry = generate_call_stub(StubRoutines::_call_stub_return_address);

現在我們終於找到了函式指標指向的函式的實現邏輯,這個邏輯是通過呼叫generate_call_stub()函式來實現的。

不過經過檢視後我們發現這個函式指標指向的並不是一個C++函式,而是一個機器指令片段,我們可以將其看為C++函式經過C++編譯器編譯後生成的指令片段即可。在generate_call_stub()函式中有如下呼叫語句:

__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);

這兩段程式碼直接生成機器指令,不過為了檢視機器指令,我們藉助了HSDB工具將其反編譯為可讀性更強的彙編指令。如下:

push   %rbp         
mov    %rsp,%rbp
sub    $0x60,%rsp

這3條彙編是非常典型的開闢新棧幀的指令。之前我們介紹過在通過函式指標進行呼叫之前的棧狀態,如下:

那麼經過執行如上3條彙編後這個棧狀態就變為了如下的狀態:

我們需要關注的就是old %rbp和old %rsp在沒有執行開闢新棧幀(CallStub()棧幀)時的指向,以及開闢新棧幀(CallStub()棧幀)時的new %rbp和new %rsp的指向。另外還要注意saved rbp儲存的就是old %rbp,這個值對於棧展開非常重要,因為能通過它不斷向上遍歷,最終能找到所有的棧幀。

下面接著看generate_call_stub()函式的實現,如下:

address generate_call_stub(address& return_address) {
    ...
    address start = __ pc();
 

    const Address rsp_after_call(rbp, rsp_after_call_off * wordSize);
 
    const Address call_wrapper  (rbp, call_wrapper_off   * wordSize);
    const Address result        (rbp, result_off         * wordSize);
    const Address result_type   (rbp, result_type_off    * wordSize);
    const Address method        (rbp, method_off         * wordSize);
    const Address entry_point   (rbp, entry_point_off    * wordSize);
    const Address parameters    (rbp, parameters_off     * wordSize);
    const Address parameter_size(rbp, parameter_size_off * wordSize);
 
    const Address thread        (rbp, thread_off         * wordSize);
 
    const Address r15_save(rbp, r15_off * wordSize);
    const Address r14_save(rbp, r14_off * wordSize);
    const Address r13_save(rbp, r13_off * wordSize);
    const Address r12_save(rbp, r12_off * wordSize);
    const Address rbx_save(rbp, rbx_off * wordSize);
 
    // 開闢新的棧幀
    __ enter();
    __ subptr(rsp, -rsp_after_call_off * wordSize);
 
    // save register parameters
    __ movptr(parameters,   c_rarg5); // parameters
    __ movptr(entry_point,  c_rarg4); // entry_point
 
 
    __ movptr(method,       c_rarg3); // method
    __ movl(result_type,  c_rarg2);   // result type
    __ movptr(result,       c_rarg1); // result
    __ movptr(call_wrapper, c_rarg0); // call wrapper
 
    // save regs belonging to calling function
    __ movptr(rbx_save, rbx);
    __ movptr(r12_save, r12);
    __ movptr(r13_save, r13);
    __ movptr(r14_save, r14);
    __ movptr(r15_save, r15);
 
    const Address mxcsr_save(rbp, mxcsr_off * wordSize);
    {
      Label skip_ldmx;
      __ stmxcsr(mxcsr_save);
      __ movl(rax, mxcsr_save);
      __ andl(rax, MXCSR_MASK);    // Only check control and mask bits
      ExternalAddress mxcsr_std(StubRoutines::addr_mxcsr_std());
      __ cmp32(rax, mxcsr_std);
      __ jcc(Assembler::equal, skip_ldmx);
      __ ldmxcsr(mxcsr_std);
      __ bind(skip_ldmx);
    }

    // ... 省略了接下來的操作
}

其中開闢新棧幀的邏輯我們已經介紹過,下面就是將call_helper()傳遞的6個在暫存器中的引數儲存到CallStub()棧幀中了,除了儲存這幾個引數外,還需要儲存其它暫存器中的值,因為函式接下來要做的操作是為Java方法準備引數並呼叫Java方法,我們並不知道Java方法會不會破壞這些暫存器中的值,所以要儲存下來,等呼叫完成後進行恢復。

生成的彙編程式碼如下:

mov      %r9,-0x8(%rbp)
mov      %r8,-0x10(%rbp)
mov      %rcx,-0x18(%rbp)
mov      %edx,-0x20(%rbp)
mov      %rsi,-0x28(%rbp)
mov      %rdi,-0x30(%rbp)
mov      %rbx,-0x38(%rbp)
mov      %r12,-0x40(%rbp)
mov      %r13,-0x48(%rbp)
mov      %r14,-0x50(%rbp)
mov      %r15,-0x58(%rbp)
// stmxcsr是將MXCSR暫存器中的值儲存到-0x60(%rbp)中
stmxcsr  -0x60(%rbp)  
mov      -0x60(%rbp),%eax
and      $0xffc0,%eax // MXCSR_MASK = 0xFFC0
// cmp通過第2個運算元減去第1個運算元的差,根據結果來設定eflags中的標誌位。
// 本質上和sub指令相同,但是不會改變運算元的值
cmp      0x1762cb5f(%rip),%eax  # 0x00007fdf5c62d2c4 
// 當ZF=1時跳轉到目標地址
je       0x00007fdf45000772 
// 將m32載入到MXCSR暫存器中
ldmxcsr  0x1762cb52(%rip)      # 0x00007fdf5c62d2c4  

載入完成這些引數後如下圖所示。

下一篇我們繼續介紹下generate_call_stub()函式中其餘的實現。

第4篇-JVM終於開始呼叫Java主類的main()方法啦

在前一篇 第3篇-CallStub新棧幀的建立 中我們介紹了generate_call_stub()函式的部分實現,完成了向CallStub棧幀中壓入引數的操作,此時的狀態如下圖所示。

繼續看generate_call_stub()函式的實現,接來下會載入執行緒暫存器,程式碼如下:

__ movptr(r15_thread, thread);
__ reinit_heapbase();

生成的彙編程式碼如下:

mov    0x18(%rbp),%r15  
mov    0x1764212b(%rip),%r12   # 0x00007fdf5c6428a8

對照著上面的棧幀可看一下0x18(%rbp)這個位置儲存的是thread,將這個引數儲存到%r15暫存器中。

如果在呼叫函式時有引數的話需要傳遞引數,程式碼如下:

Label parameters_done;
// parameter_size拷貝到c_rarg3即rcx暫存器中
__ movl(c_rarg3, parameter_size);
// 校驗c_rarg3的數值是否合法。兩運算元作與運算,僅修改標誌位,不回送結果
__ testl(c_rarg3, c_rarg3);
// 如果不合法則跳轉到parameters_done分支上
__ jcc(Assembler::zero, parameters_done);

// 如果執行下面的邏輯,那麼就表示parameter_size的值不為0,也就是需要為
// 呼叫的java方法提供引數
Label loop;
// 將地址parameters包含的資料即引數物件的指標拷貝到c_rarg2暫存器中
__ movptr(c_rarg2, parameters);       
// 將c_rarg3中值拷貝到c_rarg1中,即將引數個數複製到c_rarg1中
__ movl(c_rarg1, c_rarg3);            
__ BIND(loop);
// 將c_rarg2指向的記憶體中包含的地址複製到rax中
__ movptr(rax, Address(c_rarg2, 0));
// c_rarg2中的引數物件的指標加上指標寬度8位元組,即指向下一個引數
__ addptr(c_rarg2, wordSize);       
// 將c_rarg1中的值減一
__ decrementl(c_rarg1);            
// 傳遞方法呼叫引數
__ push(rax);                       
// 如果引數個數大於0則跳轉到loop繼續
__ jcc(Assembler::notZero, loop);

這裡是個迴圈,用於傳遞引數,相當於如下程式碼:

while(%esi){
   rax = *arg
   push_arg(rax)
   arg++;   // ptr++
   %esi--;  // counter--
}

生成的彙編程式碼如下:

// 將棧中parameter size送到%ecx中
mov    0x10(%rbp),%ecx   
// 做與運算,只有當%ecx中的值為0時才等於0 
test   %ecx,%ecx          
// 沒有引數需要傳遞,直接跳轉到parameters_done即可
je     0x00007fdf4500079a 
// -- loop --
// 彙編執行到這裡,說明paramter size不為0,需要傳遞引數
mov    -0x8(%rbp),%rdx
mov    %ecx,%esi
mov    (%rdx),%rax
add    $0x8,%rdx
dec    %esi
push   %rax
// 跳轉到loop
jne    0x00007fdf4500078e  

因為要呼叫Java方法,所以會為Java方法壓入實際的引數,也就是壓入parameter size個從parameters開始取的引數。壓入引數後的棧如下圖所示。

當把需要呼叫Java方法的引數準備就緒後,接下來就會呼叫Java方法。這裡需要重點提示一下Java解釋執行時的方法呼叫約定,不像C/C++在x86下的呼叫約定一樣,不需要通過暫存器來傳遞引數,而是通過棧來傳遞引數的,說的更直白一些,是通過區域性變數表來傳遞引數的,所以上圖CallStub()函式棧幀中的argument word1 ... argument word n其實是​被呼叫的Java方法區域性變數表的一部分。

下面接著看呼叫Java方法的程式碼,如下:

// 呼叫Java方法
// -- parameters_done --

__ BIND(parameters_done); 
// 將method地址包含的資料接Method*拷貝到rbx中
__ movptr(rbx, method);            
// 將直譯器的入口地址拷貝到c_rarg1暫存器中
__ movptr(c_rarg1, entry_point);    
// 將rsp暫存器的資料拷貝到r13暫存器中
__ mov(r13, rsp);                   

// 呼叫直譯器的解釋函式,從而呼叫Java方法
// 呼叫的時候傳遞c_rarg1,也就是直譯器的入口地址
__ call(c_rarg1); 

生成的彙編程式碼如下:

// 將Method*送到%rbx中
mov     -0x18(%rbp),%rbx  
// 將entry_point送到%rsi中
mov     -0x10(%rbp),%rsi  
// 將呼叫者的棧頂指標儲存到%r13中
mov     %rsp,%r13    
// 呼叫Java方法     
callq   *%rsi             

注意呼叫callq指令後,會將callq指令的下一條指令的地址壓棧,再跳轉到第1運算元指定的地址,也就是*%rsi表示的地址。壓入下一條指令的地址是為了讓函式能通過跳轉到棧上的地址從子函式返回。 

callq指令呼叫的是entry_point。entry_point在後面會詳細介紹。

第5篇-呼叫Java方法後彈出棧幀及處理返回結果

在前一篇第4篇-JVM終於開始呼叫Java主類的main()方法啦介紹了通過callq呼叫entry point,不過我們並沒有看完generate_call_stub()函式的實現。接下來在generate_call_stub()函式中會處理呼叫Java方法後的返回值,同時還需要執行退棧操作,也就是將棧恢復到呼叫Java方法之前的狀態。呼叫之前是什麼狀態呢?在 第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法 中介紹過,這個狀態如下圖所示。

generate_call_stub()函式接下來的程式碼實現如下:

// 儲存方法呼叫結果依賴於結果型別,只要不是T_OBJECT, T_LONG, T_FLOAT or T_DOUBLE,都當做T_INT處理
// 將result地址的值拷貝到c_rarg0中,也就是將方法呼叫的結果儲存在rdi暫存器中,注意result為函式返回值的地址
__ movptr(c_rarg0, result);     

Label is_long, is_float, is_double, exit;

// 將result_type地址的值拷貝到c_rarg1中,也就是將方法呼叫的結果返回的型別儲存在esi暫存器中
__ movl(c_rarg1, result_type);  

// 根據結果型別的不同跳轉到不同的處理分支
__ cmpl(c_rarg1, T_OBJECT);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(c_rarg1, T_DOUBLE);
__ jcc(Assembler::equal, is_double);

// 當邏輯執行到這裡時,處理的就是T_INT型別,
// 將rax中的值寫入c_rarg0儲存的地址指向的記憶體中
// 呼叫函式後如果返回值是int型別,則根據呼叫約定
// 會儲存在eax中
__ movl(Address(c_rarg0, 0), rax); 

__ BIND(exit);


// 將rsp_after_call中儲存的有效地址拷貝到rsp中,即將rsp往高地址方向移動了,
// 原來的方法呼叫實參argument 1、...、argument n,
// 相當於從棧中彈出,所以下面語句執行的是退棧操作
__ lea(rsp, rsp_after_call);  // lea指令將地址載入到暫存器中

這裡我們要關注result和result_type,result在呼叫call_helper()函式時就會傳遞,也就是會指示call_helper()函式將呼叫Java方法後的返回值儲存在哪裡。對於型別為JavaValue的result來說,其實在呼叫之前就已經設定了返回型別,所以如上的result_type變數只需要從JavaValue中獲取結果型別即可。例如,呼叫Java主類的main()方法時,在jni_CallStaticVoidMethod()函式和jni_invoke_static()函式中會設定返回型別為T_VOID,也就是main()方法返回void。

生成的彙編程式碼如下:

// 棧中的-0x28位置儲存result
mov    -0x28(%rbp),%rdi  
// 棧中的-0x20位置儲存result type
mov    -0x20(%rbp),%esi  
cmp    $0xc,%esi         // 是否為T_OBJECT型別
je     0x00007fdf450007f6
cmp    $0xb,%esi         // 是否為T_LONG型別
je     0x00007fdf450007f6
cmp    $0x6,%esi         // 是否為T_FLOAT型別
je     0x00007fdf450007fb
cmp    $0x7,%esi         // 是否為T_DOUBLE型別
je     0x00007fdf45000801

// 如果是T_INT型別,直接將返回結果%eax寫到棧中-0x28(%rbp)的位置
mov    %eax,(%rdi)       

// -- exit --

// 將rsp_after_call的有效地址拷到rsp中
lea    -0x60(%rbp),%rsp  

為了讓大家看清楚,我貼一下在呼叫Java方法之前的棧幀狀態,如下:

由圖可看到-0x60(%rbp)地址指向的位置,恰好不包括呼叫Java方法時壓入的實際引數argument word 1 ... argument word n。所以現在rbp和rsp就是圖中指向的位置了。

接下來恢復之前儲存的caller-save暫存器,這也是呼叫約定的一部分,如下:

__ movptr(r15, r15_save);
__ movptr(r14, r14_save);
__ movptr(r13, r13_save);
__ movptr(r12, r12_save);
__ movptr(rbx, rbx_save);

__ ldmxcsr(mxcsr_save); 

生成的彙編程式碼如下:

mov      -0x58(%rbp),%r15
mov      -0x50(%rbp),%r14
mov      -0x48(%rbp),%r13
mov      -0x40(%rbp),%r12
mov      -0x38(%rbp),%rbx
ldmxcsr  -0x60(%rbp)

在彈出了為呼叫Java方法儲存的實際引數及恢復caller-save暫存器後,繼續執行退棧操作,實現如下:

// restore rsp
__ addptr(rsp, -rsp_after_call_off * wordSize);

// return
__ pop(rbp);
__ ret(0);

生成的彙編程式碼如下:

// %rsp加上0x60,也就是執行退棧操作,也就相
// 當於彈出了callee_save暫存器和壓棧的那6個引數
add    $0x60,%rsp 
pop    %rbp
// 方法返回,指令中的q表示64位運算元,就是指
// 的棧中儲存的return address是64位的
retq  

記得在之前 第3篇-CallStub新棧幀的建立時,通過如下的彙編完成了新棧幀的建立:

push   %rbp         
mov    %rsp,%rbp 
sub    $0x60,%rsp

現在要退出這個棧幀時要在%rsp指向的地址加上$0x60,同時恢復%rbp的指向。然後就是跳轉到return address指向的指令繼續執行了。

為了方便大家檢視,我再次給出了之前使用到的圖片,這個圖是退棧之前的圖片:

退棧之後如下圖所示。

至於paramter size與thread則由JavaCalls::call_hlper()函式負責釋放,這是C/C++呼叫約定的一部分。所以如果不看這2個引數,我們已經完全回到了本篇給出的第一張圖表示的棧的樣子。

上面這些圖片大家應該不陌生才對,我們在一步步建立棧幀時都給出過,現在怎麼建立的就會怎麼退出。

之前介紹過,當Java方法返回int型別時(如果返回char、boolean、short等型別時統一轉換為int型別),根據Java方法呼叫約定,這個返回的int值會儲存到%rax中;如果返回物件,那麼%rax中儲存的就是這個物件的地址,那後面到底怎麼區分是地址還是int值呢?答案是通過返回型別區分即可;如果返回非int,非物件型別的值呢?我們繼續看generate_call_stub()函式的實現邏輯:

// handle return types different from T_INT
__ BIND(is_long);
__ movq(Address(c_rarg0, 0), rax);
__ jmp(exit);

__ BIND(is_float);
__ movflt(Address(c_rarg0, 0), xmm0);
__ jmp(exit);

__ BIND(is_double);
__ movdbl(Address(c_rarg0, 0), xmm0);
__ jmp(exit); 

對應的彙編程式碼如下:

// -- is_long --
mov    %rax,(%rdi)
jmp    0x00007fdf450007d4

// -- is_float --
vmovss %xmm0,(%rdi)
jmp    0x00007fdf450007d4

// -- is_double --
vmovsd %xmm0,(%rdi)
jmp    0x00007fdf450007d4

當返回long型別時也儲存到%rax中,因為Java的long型別是64位,我們分析的程式碼也是x86下64位的實現,所以%rax暫存器也是64位,能夠容納64位數;當返回為float或double時,儲存到%xmm0中。

統合這一篇和前幾篇文章,我們應該學習到C/C++的呼叫約定以及Java方法在解釋執行下的呼叫約定(包括如何傳遞引數,如何接收返回值等),如果大家不明白,多讀幾遍文章就會有一個清晰的認識。

第6篇-Java方法新棧幀的建立

第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法介紹JavaCalls::call_helper()函式的實現時提到過如下一句程式碼:

address entry_point = method->from_interpreted_entry();

這個引數會做為實參傳遞給StubRoutines::call_stub()函式指標指向的“函式”,然後在 第4篇-JVM終於開始呼叫Java主類的main()方法啦 介紹到通過callq指令呼叫entry_point,那麼這個entry_point到底是什麼呢?這一篇我們將詳細介紹。

首先看from_interpreted_entry()函式實現,如下:

原始碼位置:/openjdk/hotspot/src/share/vm/oops/method.hpp

volatile address from_interpreted_entry() const{ 
      return (address)OrderAccess::load_ptr_acquire(&_from_interpreted_entry); 
}

_from_interpreted_entry只是Method類中定義的一個屬性,如上方法直接返回了這個屬性的值。那麼這個屬性是何時賦值的?其實是在方法連線(也就是在類的生命週期中的類連線階段會進行方法連線)時會設定。方法連線時會呼叫如下方法:

void Method::link_method(methodHandle h_method, TRAPS) {
  // ...
  address entry = Interpreter::entry_for_method(h_method);
  // Sets both _i2i_entry and _from_interpreted_entry
  set_interpreter_entry(entry);
  // ...
}

首先呼叫Interpreter::entry_for_method()函式根據特定方法型別獲取到方法的入口,得到入口entry後會呼叫set_interpreter_entry()函式將值儲存到對應屬性上。set_interpreter_entry()函式的實現非常簡單,如下:

void set_interpreter_entry(address entry) { 
    _i2i_entry = entry;  
    _from_interpreted_entry = entry; 
}

可以看到為_from_interpreted_entry屬性設定了entry值。

下面看一下entry_for_method()函式的實現,如下:

static address entry_for_method(methodHandle m)  { 
     return entry_for_kind(method_kind(m)); 
}

首先通過method_kind()函式拿到方法對應的型別,然後呼叫entry_for_kind()函式根據方法型別獲取方法對應的入口entry_point。呼叫的entry_for_kind()函式的實現如下:

static address entry_for_kind(MethodKind k){ 
      return _entry_table[k]; 
}

這裡直接返回了_entry_table陣列中對應方法型別的entry_point地址。

這裡涉及到Java方法的型別MethodKind,由於要通過entry_point進入Java世界,執行Java方法相關的邏輯,所以entry_point中一定會為對應的Java方法建立新的棧幀,但是不同方法的棧幀其實是有差別的,如Java普通方法、Java同步方法、有native關鍵字的Java方法等,所以就把所有的方法進行了歸類,不同型別獲取到不同的entry_point入口。到底有哪些型別,我們可以看一下MethodKind這個列舉類中定義出的列舉常量:

enum MethodKind {
    zerolocals,  // 普通的方法             
    zerolocals_synchronized,  // 普通的同步方法         
    native,  // native方法
    native_synchronized,  // native同步方法
    ...
}

當然還有其它一些型別,不過最主要的就是如上列舉類中定義出的4種型別方法。

為了能儘快找到某個Java方法對應的entry_point入口,把這種對應關係儲存到了_entry_table中,所以entry_for_kind()函式才能快速的獲取到方法對應的entry_point入口。 給陣列中元素賦值專門有個方法:

void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) {
  _entry_table[kind] = entry;
}

那麼何時會呼叫set_entry_for_kind()函式呢,答案就在TemplateInterpreterGenerator::generate_all()函式中,generate_all()函式會呼叫generate_method_entry()函式生成每種Java方法的entry_point,每生成一個對應方法型別的entry_point就儲存到_entry_table中。

下面詳細介紹一下generate_all()函式的實現邏輯,在HotSpot啟動時就會呼叫這個函式生成各種Java方法的entry_point。呼叫棧如下:

TemplateInterpreterGenerator::generate_all()  templateInterpreter.cpp
InterpreterGenerator::InterpreterGenerator()  templateInterpreter_x86_64.cpp
TemplateInterpreter::initialize()    templateInterpreter.cpp
interpreter_init()                   interpreter.cpp
init_globals()                       init.cpp
Threads::create_vm()                 thread.cpp
JNI_CreateJavaVM()                   jni.cpp
InitializeJVM()                      java.c
JavaMain()                           java.c
start_thread()                       pthread_create.c

呼叫的generate_all()函式將生成一系列HotSpot執行過程中所執行的一些公共程式碼的入口和所有位元組碼的InterpreterCodelet,一些非常重要的入口實現邏輯會在後面詳細介紹,這裡只看普通的、沒有native關鍵字修飾的Java方法生成入口的邏輯。generate_all()函式中有如下實現:

#define method_entry(kind)                                                                    \
{                                                                                             \
    CodeletMark cm(_masm, "method entry point (kind = " #kind ")");                           \
    Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind);  \
}  

method_entry(zerolocals)

其中method_entry是一個巨集,擴充套件後如上的method_entry(zerolocals)語句變為如下的形式:

Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);

_entry_table變數定義在AbstractInterpreter類中,如下:

static address  _entry_table[number_of_method_entries];  

number_of_method_entries表示方法型別的總數,使用方法型別做為陣列下標就可以獲取對應的方法入口。呼叫generate_method_entry()函式為各種型別的方法生成對應的方法入口。generate_method_entry()函式的實現如下:

address AbstractInterpreterGenerator::generate_method_entry(AbstractInterpreter::MethodKind kind) {
  bool                   synchronized = false;
  address                entry_point = NULL;
  InterpreterGenerator*  ig_this = (InterpreterGenerator*)this;

  // 根據方法型別kind生成不同的入口
  switch (kind) { 
  // 表示普通方法型別
  case Interpreter::zerolocals :
      break;
  // 表示普通的、同步方法型別
  case Interpreter::zerolocals_synchronized: 
      synchronized = true;
      break;
  // ...
  }

  if (entry_point) {
     return entry_point;
  }

  return ig_this->generate_normal_entry(synchronized);
}

zerolocals表示正常的Java方法呼叫,包括Java程式的main()方法,對於zerolocals來說,會呼叫ig_this->generate_normal_entry()函式生成入口。generate_normal_entry()函式會為執行的方法生成堆疊,而堆疊由區域性變數表(用來儲存傳入的引數和被呼叫方法的區域性變數)、Java方法棧幀資料和運算元棧這三大部分組成,所以entry_point例程(其實就是一段機器指令片段,英文名為stub)會建立這3部分來輔助Java方法的執行。

我們還是回到開篇介紹的知識點,通過callq指令呼叫entry_point例程。此時的棧幀狀態在 第4篇-JVM終於開始呼叫Java主類的main()方法啦 中介紹過,為了大家閱讀的方便,這裡再次給出:

注意,在執行callq指令時,會將函式的返回地址儲存到棧頂,所以上圖中會壓入return address一項。

CallStub()函式在通過callq指令呼叫generate_normal_entry()函式生成的entry_point時,有幾個暫存器中儲存著重要的值,如下:

rbx -> Method*
r13 -> sender sp
rsi -> entry point

下面就是分析generate_normal_entry()函式的實現邏輯了,這是呼叫Java方法的最重要的部分。函式的重要實現邏輯如下:

address InterpreterGenerator::generate_normal_entry(bool synchronized) {
  // ...
  // entry_point函式的程式碼入口地址
  address entry_point = __ pc();   
 
  // 當前rbx中儲存的是指向Method的指標,通過Method*找到ConstMethod*
  const Address constMethod(rbx, Method::const_offset()); 
  // 通過Method*找到AccessFlags
  const Address access_flags(rbx, Method::access_flags_offset()); 
  // 通過ConstMethod*得到parameter的大小
  const Address size_of_parameters(rdx,ConstMethod::size_of_parameters_offset());
  // 通過ConstMethod*得到local變數的大小
  const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());
 
  // 上面已經說明了獲取各種方法後設資料的計算方式,
  // 但並沒有執行計算,下面會生成對應的彙編來執行計算
  // 計算ConstMethod*,儲存在rdx裡面
  __ movptr(rdx, constMethod);                    
  // 計算parameter大小,儲存在rcx裡面 
  __ load_unsigned_short(rcx, size_of_parameters);

  // rbx:儲存基址;rcx:儲存迴圈變數;rdx:儲存目標地址;rax:儲存返回地址(下面用到)
  // 此時的各個暫存器中的值如下:
  //   rbx: Method*
  //   rcx: size of parameters
  //   r13: sender_sp (could differ from sp+wordSize 
  //        if we were called via c2i ) 即呼叫者的棧頂地址
  // 計算local變數的大小,儲存到rdx
  __ load_unsigned_short(rdx, size_of_locals);
  // 由於區域性變數表用來儲存傳入的引數和被呼叫方法的區域性變數,
  // 所以rdx減去rcx後就是被呼叫方法的區域性變數可使用的大小 
  __ subl(rdx, rcx); 
 
  // ...
 
  // 返回地址是在CallStub中儲存的,如果不彈出堆疊到rax,中間
  // 會有個return address使的區域性變數表不是連續的,
  // 這會導致其中的區域性變數計算方式不一致,所以暫時將返
  // 回地址儲存到rax中
  __ pop(rax);
 
  // 計算第1個引數的地址:當前棧頂地址 + 變數大小 * 8 - 一個字大小
  // 注意,因為地址儲存在低地址上,而堆疊是向低地址擴充套件的,所以只
  // 需加n-1個變數大小就可以得到第1個引數的地址
  __ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize));
 
  // 把函式的區域性變數設定為0,也就是做初始化,防止之前遺留下的值影響
  // rdx:被呼叫方法的區域性變數可使用的大小
  {
    Label exit, loop;
    __ testl(rdx, rdx);
    // 如果rdx<=0,不做任何操作
    __ jcc(Assembler::lessEqual, exit); 
    __ bind(loop);
    // 初始化區域性變數
    __ push((int) NULL_WORD); 
    __ decrementl(rdx); 
    __ jcc(Assembler::greater, loop);
    __ bind(exit);
  }
 
  // 生成固定楨
  generate_fixed_frame(false);
 
  // ... 省略統計及棧溢位等邏輯,後面會詳細介紹

  // 如果是同步方法時,還需要執行lock_method()函式,所以
  // 會影響到棧幀佈局 
  if (synchronized) {
    // Allocate monitor and lock method
    lock_method();
  } 

  // 跳轉到目標Java方法的第一條位元組碼指令,並執行其對應的機器指令
   __ dispatch_next(vtos);
 
  // ... 省略統計相關邏輯,後面會詳細介紹
 
  return entry_point;
}

這個函式的實現看起來比較多,但其實邏輯實現比較簡單,就是根據被呼叫方法的實際情況建立出對應的區域性變數表,然後就是2個非常重要的函式generate_fixed_frame()和dispatch_next()函式了,這2個函式我們後面再詳細介紹。

在呼叫generate_fixed_frame()函式之前,棧的狀態變為了下圖所示的狀態。

與前一個圖對比一下,可以看到多了一些local variable 1 ... local variable n等slot,這些slot與argument word 1 ... argument word n共同構成了被呼叫的Java方法的區域性變數表,也就是圖中紫色的部分。其實local variable 1 ... local variable n等slot屬於被呼叫的Java方法棧幀的一部分,而argument word 1 ... argument word n卻屬於CallStub()函式棧幀的一部分,這2部分共同構成區域性變數表,專業術語叫棧幀重疊。

另外還能看出來,%r14指向了區域性變數表的第1個引數,而CallStub()函式的return address被儲存到了%rax中,另外%rbx中依然儲存著Method*。這些暫存器中儲存的值將在呼叫generate_fixed_frame()函式時用到,所以我們需要在這裡強調一下。

第7篇-為Java方法建立棧幀

第6篇-Java方法新棧幀的建立介紹過區域性變數表的建立,建立完成後的棧幀狀態如下圖所示。

各個暫存器的狀態如下所示。

// %rax暫存器中儲存的是返回地址
rax: return address     
// 要執行的Java方法的指標
rbx: Method*          
// 本地變數表指標  
r14: pointer to locals 
// 呼叫者的棧頂
r13: sender sp 

注意rax中儲存的返回地址,因為在generate_call_stub()函式中通過__ call(c_rarg1) 語句呼叫了由generate_normal_entry()函式生成的entry_point,所以當entry_point執行完成後,還會返回到generate_call_stub()函式中繼續執行__ call(c_rarg1) 語句下面的程式碼,也就是

第5篇-呼叫Java方法後彈出棧幀及處理返回結果涉及到的那些程式碼。

呼叫的generate_fixed_frame()函式的實現如下:

原始碼位置:openjdk/hotspot/src/cpu/x86/vm/templateInterpreter_x86_64.cpp

void TemplateInterpreterGenerator::generate_fixed_frame(bool native_call) {
  // 把返回地址緊接著區域性變數區儲存
  __ push(rax);     
  // 為Java方法建立棧幀       
  __ enter();      
  // 儲存呼叫者的棧頂地址        
  __ push(r13);           
   // 暫時將last_sp屬性的值設定為NULL_WORD 
  __ push((int)NULL_WORD); 
  // 獲取ConstMethod*並儲存到r13中
  __ movptr(r13, Address(rbx, Method::const_offset()));     
  // 儲存Java方法位元組碼的地址到r13中
  __ lea(r13, Address(r13, ConstMethod::codes_offset()));    
  // 儲存Method*到堆疊上
  __ push(rbx);             
 
  // ProfileInterpreter屬性的預設值為true,
  // 表示需要對解釋執行的方法進行相關資訊的統計
  if (ProfileInterpreter) {
    Label method_data_continue;
    // MethodData結構基礎是ProfileData,
    // 記錄函式執行狀態下的資料
    // MethodData裡面分為3個部分,
    // 一個是函式型別等執行相關統計資料,
    // 一個是引數型別執行相關統計資料,
    // 還有一個是extra擴充套件區儲存著
    // deoptimization的相關資訊
    // 獲取Method中的_method_data屬性的值並儲存到rdx中
    __ movptr(rdx, Address(rbx,
           in_bytes(Method::method_data_offset())));
    __ testptr(rdx, rdx);
    __ jcc(Assembler::zero, method_data_continue);
    // 執行到這裡,說明_method_data已經進行了初始化,
    // 通過MethodData來獲取_data屬性的值並儲存到rdx中
    __ addptr(rdx, in_bytes(MethodData::data_offset()));
    __ bind(method_data_continue);
    __ push(rdx);      
  } else {
    __ push(0);
  }
  
  // 獲取ConstMethod*儲存到rdx
  __ movptr(rdx, Address(rbx, 
        Method::const_offset()));          
  // 獲取ConstantPool*儲存到rdx
  __ movptr(rdx, Address(rdx, 
         ConstMethod::constants_offset())); 
 // 獲取ConstantPoolCache*並儲存到rdx
  __ movptr(rdx, Address(rdx, 
         ConstantPool::cache_offset_in_bytes())); 
  // 儲存ConstantPoolCache*到堆疊上
  __ push(rdx); 
  // 儲存第1個引數的地址到堆疊上
  __ push(r14); 
 
  if (native_call) {
   // native方法呼叫時,不需要儲存Java
   // 方法的位元組碼地址,因為沒有位元組碼
    __ push(0); 
  } else {
   // 儲存Java方法位元組碼地址到堆疊上,
   // 注意上面對r13暫存器的值進行了更改
    __ push(r13);
  }
  
  // 預先保留一個slot,後面有大用處
  __ push(0); 
  // 將棧底地址儲存到這個slot上
  __ movptr(Address(rsp, 0), rsp); 
}

對於普通的Java方法來說,生成的彙編程式碼如下:  

push   %rax
push   %rbp
mov    %rsp,%rbp
push   %r13
pushq  $0x0
mov    0x10(%rbx),%r13
lea    0x30(%r13),%r13 // lea指令獲取記憶體地址本身
push   %rbx
mov    0x18(%rbx),%rdx
test   %rdx,%rdx
je     0x00007fffed01b27d
add    $0x90,%rdx
push   %rdx
mov    0x10(%rbx),%rdx
mov    0x8(%rdx),%rdx
mov    0x18(%rdx),%rdx
push   %rdx
push   %r14
push   %r13
pushq  $0x0
mov    %rsp,(%rsp)

彙編比較簡單,這裡不再多說。執行完如上的彙編後生成的棧幀狀態如下圖所示。

呼叫完generate_fixed_frame()函式後一些暫存器中儲存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地變數表第1個引數的地址

執行完generate_fixed_frame()函式後會繼續返回執行InterpreterGenerator::generate_normal_entry()函式,如果是為同步方法生成機器碼,那麼還需要呼叫lock_method()函式,這個函式會改變當前棧幀的狀態,新增同步所需要的一些資訊,在後面介紹鎖的實現時會詳細介紹。

InterpreterGenerator::generate_normal_entry()函式最終會返回生成機器碼的入口執行地址,然後通過變數_entry_table陣列來儲存,這樣就可以使用方法型別做為陣列下標獲取對應的方法入口了。

第8篇-dispatch_next()函式分派位元組碼

在generate_normal_entry()函式中會呼叫generate_fixed_frame()函式為Java方法的執行生成對應的棧幀,接下來還會呼叫dispatch_next()函式執行Java方法的位元組碼。generate_normal_entry()函式呼叫的dispatch_next()函式之前一些暫存器中儲存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地變數表第1個引數的地址

dispatch_next()函式的實現如下:

// 從generate_fixed_frame()函式生成Java方法呼叫棧幀的時候,
// 如果當前是第一次呼叫,那麼r13指向的是位元組碼的首地址,
// 即第一個位元組碼,此時的step引數為0
void InterpreterMacroAssembler::dispatch_next(TosState state, int step) {
 
  load_unsigned_byte(rbx, Address(r13, step)); 
 
  // 在當前位元組碼的位置,指標向前移動step寬度,
  // 獲取地址上的值,這個值是Opcode(範圍1~202),儲存到rbx
  // step的值由位元組碼指令和它的運算元共同決定
  // 自增r13供下一次位元組碼分派使用
  increment(r13, step);
 
  // 返回當前棧頂狀態的所有位元組碼入口點
  dispatch_base(state, Interpreter::dispatch_table(state)); 
}

r13指向位元組碼的首地址,當第1次呼叫時,引數step的值為0,那麼load_unsigned_byte()函式從r13指向的記憶體中取一個位元組的值,取出來的是位元組碼指令的操作碼。增加r13的步長,這樣下次執行時就會取出來下一個位元組碼指令的操作碼。

呼叫的dispatch_table()函式的實現如下:

static address*   dispatch_table(TosState state)  {
   return _active_table.table_for(state); 
}

在_active_table中獲取對應棧頂快取狀態的入口地址,_active_table變數定義在TemplateInterpreter類中,如下:

static DispatchTable  _active_table;  

DispatchTable類及table_for()等函式的定義如下:

DispatchTable  TemplateInterpreter::_active_table;

class DispatchTable VALUE_OBJ_CLASS_SPEC {
 public:
  enum { 
    length = 1 << BitsPerByte 
  }; // BitsPerByte的值為8
 
 private:
  // number_of_states=9,length=256
  // _table是位元組碼分發表 
  address  _table[number_of_states][length];   
 
 public:
  // ...
  address*   table_for(TosState state){ 
    return _table[state]; 
  }

  address*   table_for(){ 
    return table_for((TosState)0); 
  }
  // ...
}; 

address為u_char*型別的別名。_table是一個二維陣列的表,維度為棧頂狀態(共有9種)和位元組碼(最多有256個),儲存的是每個棧頂狀態對應的位元組碼的入口點。這裡由於還沒有介紹棧頂快取,所以理解起來並不容易,不過後面會詳細介紹棧頂快取和位元組碼分發表的相關內容,等介紹完了再看這部分邏輯就比較容易理解了。

InterpreterMacroAssembler::dispatch_next()函式中呼叫的dispatch_base()函式的實現如下:

void InterpreterMacroAssembler::dispatch_base(
  TosState  state, // 表示棧頂快取狀態
  address*  table,
  bool verifyoop
) {
  // ...
  // 獲取當前棧頂狀態位元組碼轉發表的地址,儲存到rscratch1
  lea(rscratch1, ExternalAddress((address)table));
  // 跳轉到位元組碼對應的入口執行機器碼指令
  // address = rscratch1 + rbx * 8
  jmp(Address(rscratch1, rbx, Address::times_8));
} 

比如取一個位元組大小的指令(如iconst_0、aload_0等都是一個位元組大小的指令),那麼InterpreterMacroAssembler::dispatch_next()函式生成的彙編程式碼如下 :

// 在generate_fixed_frame()函式中
// 已經讓%r13儲存了bcp
// %ebx中儲存的是位元組碼的Opcode,也就是操作碼
movzbl 0x0(%r13),%ebx  
 
// $0x7ffff73ba4a0這個地址指向的
// 是對應state狀態下的一維陣列,長度為256
movabs $0x7ffff73ba4a0,%r10

// 注意%r10中儲存的是常量,根據計算公式
// %r10+%rbx*8來獲取指向儲存入口地址的地址,
// 通過*(%r10+%rbx*8)獲取到入口地址,
// 然後跳轉到入口地址執行
jmpq *(%r10,%rbx,8)

%r10指向的是對應棧頂快取狀態state下的一維陣列,長度為256,其中儲存的值為opcode,如下圖所示。

下面的函式顯示了對每個位元組碼的每個棧頂狀態都設定入口地址。

void DispatchTable::set_entry(int i, EntryPoint& entry) {
  assert(0 <= i && i < length, "index out of bounds");
  assert(number_of_states == 9, "check the code below");
  _table[btos][i] = entry.entry(btos);
  _table[ctos][i] = entry.entry(ctos);
  _table[stos][i] = entry.entry(stos);
  _table[atos][i] = entry.entry(atos);
  _table[itos][i] = entry.entry(itos);
  _table[ltos][i] = entry.entry(ltos);
  _table[ftos][i] = entry.entry(ftos);
  _table[dtos][i] = entry.entry(dtos);
  _table[vtos][i] = entry.entry(vtos);
}

其中的引數i就是opcode,各個位元組碼及對應的opcode可參考https://docs.oracle.com/javas...

所以_table表如下圖所示。

_table的一維為棧頂快取狀態,二維為Opcode,通過這2個維度能夠找到一段機器指令,這就是根據當前的棧頂快取狀態定位到的位元組碼需要執行的機器指令片段。

呼叫dispatch_next()函式執行Java方法的位元組碼,其實就是根據位元組碼找到對應的機器指令片段的入口地址來執行,這段機器碼就是根據對應的位元組碼語義翻譯過來的,這些都會在後面詳細介紹。

公眾號【深入剖析Java虛擬機器HotSpot】已經更新虛擬機器原始碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機器群交流。

相關文章