第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()函數時用到,所以我們需要在這里強調一下。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM