1. 程式人生 > >JVM的方法執行引擎-entry point棧幀

JVM的方法執行引擎-entry point棧幀

接著上一篇去講,回到JavaCalls::call_helper()中:

address entry_point = method->from_interpreted_entry();

entry_point是從當前要執行的Java方法中獲取的,定義如下:

原始碼位置:/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.hpp中有這樣一個set方法:

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

在連線方法時通過如下的方法呼叫上面的方法:

// Called when the method_holder is getting linked. Setup entrypoints so the method
// is ready to be called from interpreter, compiler, and vtables.
void Method::link_method(methodHandle h_method, TRAPS) {
  // ...
  address entry = Interpreter::entry_for_method(h_method);
  assert(entry != NULL, "interpreter entry must be non-null");
  // Sets both _i2i_entry and _from_interpreted_entry
  set_interpreter_entry(entry);
  // ...
}

根據註釋都可以得知,當方法連線時,會去設定方法的entry_point,entry_point是通過呼叫Interpreter::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地址。給陣列中元素賦值專門有個方法:

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()去生成每種方法的entry_point,所有Java方法的執行,都會通過對應型別的entry_point例程來輔助。下面來詳細介紹一下generate_all()方法的實現邏輯。 

HotSpot在啟動時,會為所有位元組碼建立在特定目標平臺上執行的機器碼,並存放在CodeCache中,在解釋執行位元組碼的過程中,就會從CodeCache中取出這些本地機器碼並執行。

在啟動虛擬機器階段會呼叫init_globals()方法初始化全域性模組,在這個方法中通過呼叫interpreter_init()方法初始化模板直譯器,呼叫棧如下:

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	

interpreter_init()方法主要是通過呼叫TemplateInterpreter::initialize()方法來完成邏輯,initialize()方法的實現如下:

原始碼位置:/src/share/vm/interpreter/templateInterpreter.cpp

void TemplateInterpreter::initialize() {
  if (_code != NULL) 
       return;

  // 抽象直譯器AbstractInterpreter的初始化,AbstractInterpreter是基於彙編模型的直譯器的共同基類,
  // 定義瞭解釋器和直譯器生成器的抽象介面
  AbstractInterpreter::initialize();

  // 模板表TemplateTable的初始化,模板表TemplateTable儲存了各個位元組碼的模板
  TemplateTable::initialize();

  // generate interpreter
  {
     ResourceMark rm;
     int code_size = InterpreterCodeSize;
     // CodeCache的Stub佇列StubQueue的初始化
     _code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,"Interpreter");
     //  例項化模板直譯器生成器物件TemplateInterpreterGenerator
     InterpreterGenerator g(_code);
  }

  // initialize dispatch table
  _active_table = _normal_table;
}

模板直譯器的初始化包括如下幾個方面:

(1)抽象直譯器AbstractInterpreter的初始化,AbstractInterpreter是基於彙編模型的直譯器的共同基類,定義瞭解釋器和直譯器生成器的抽象介面。

(2)模板表TemplateTable的初始化,模板表TemplateTable儲存了各個位元組碼的模板(目的碼生成函式和引數);

(3)CodeCache的Stub佇列StubQueue的初始化;

(4)直譯器生成器InterpreterGenerator的初始化。

在執行InterpreterGenerator g(_code)程式碼時,呼叫InterpreterGenerator的建構函式,如下:

InterpreterGenerator::InterpreterGenerator(StubQueue* code) : TemplateInterpreterGenerator(code) {
   generate_all(); // down here so it can be "virtual"
}

呼叫的generate_all()方法將生成一系列HotSpot執行過程中所執行的一些公共程式碼的入口和所有位元組碼的InterpreterCodelet。這些入口包括:

  • error exits:出錯退出處理入口
  • 位元組碼追蹤入口(配置了-XX:+TraceBytecodes)
  • 函式返回入口
  • JVMTI的EarlyReturn入口
  • 逆優化呼叫返回入口
  • native呼叫返回值處理handlers入口
  • continuation入口
  • safepoint入口
  • 異常處理入口
  • 丟擲異常入口
  • 方法入口(native方法和非native方法)
  • 位元組碼入口

部分重要的入口實現邏輯會在後面詳細介紹,這裡只看為非native方法入口(也就是普通的、沒有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是巨集,擴充套件後如上的呼叫語句變為如下的形式:

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

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

// method entry points
static address    _entry_table[number_of_method_entries];     // entry points for a given method

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

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

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

  if (entry_point) {
     return entry_point;
  }

  return ig_this->generate_normal_entry(synchronized);
}

zerolocals表示正常的Java方法呼叫(包括Java程式的主函式),對於zerolocals來說,會呼叫ig_this->generate_normal_entry()方法生成入口。generate_normal_entry()方法會為執行的方法生成堆疊,而堆疊由區域性變量表(用來儲存傳入的引數和被呼叫函式的區域性變數)、幀資料和運算元棧這三大部分組成,所以方法會建立這3部分來輔助Java方法的執行。

之前在介紹CallStub棧幀時講到過,如果要執行entry_point,那麼棧幀的狀態就如下圖所示。

 

/src/cpu/x86/vm/templateInterpreter_x86_64.cpp檔案中generate_normal_entry()方法在通過CallStub呼叫時,各個暫存器的狀態如下:

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

generate_normal_entry()方法的實現如下:

// Generic interpreted method entry to (asm) interpreter
address InterpreterGenerator::generate_normal_entry(bool synchronized) {
  // determine code generation flags
  bool inc_counter  = UseCompiler || CountCompiledCalls;
 
  // 執行如下方法前的暫存器中儲存的值如下:
  // ebx: Method*
  // r13: sender sp  
  address entry_point = __ pc();   // entry_point函式的程式碼入口地址
 
  // 當前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());
 
  // 上面已經說明了獲取各種方法元資料的計算方式,但並沒有執行計算,下面會生成對應的彙編來執行計算
  // get parameter size (always needed)
  __ movptr(rdx, constMethod);                     // 計算ConstMethod*,儲存在rdx裡面
  __ load_unsigned_short(rcx, size_of_parameters); // 計算parameter大小,儲存在rcx裡面
  //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); 
 
 
  // see if we've got enough room on the stack for locals plus overhead.
  generate_stack_overflow_check();
 
  //返回地址是在call_stub中儲存的,如果不彈出堆疊到rax,那麼區域性變數區就如下面的樣子:
  // [parameter 1]
  // [parameter 2]
  // ......
  // [parameter n]
  // [return address]
  // [local 1]
  // [local 2]
  // ...
  // [local n]
  // 顯然中間有個return address使的區域性變量表不是連續的,這會導致其中的區域性變數計算方式不一致,所以暫時將返回地址儲存到rax中
  // get return address
  __ pop(rax);
 
  // compute beginning of parameters (r14)
  // 計算第1個引數的地址:當前棧頂地址 + 變數大小 * 8 - 一個字大小。
  // 這兒注意,因為地址儲存在低地址上,而堆疊是向低地址擴充套件的,所以只需加n-1個變數大小就可以得到第1個引數的地址。
  __ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize));
 
  // 把函式的區域性變數全置為0,也就是做初始化,防止之前遺留下的值影響
  // rdx:被呼叫函式的區域性變數可使用的大小
  // allocate space for locals
  // explicitly initialize locals
  {
    Label exit, loop;
    __ testl(rdx, rdx);
    __ jcc(Assembler::lessEqual, exit); // do nothing if rdx <= 0
    __ bind(loop);
    __ push((int) NULL_WORD); // initialize local variables
    __ decrementl(rdx); // until everything initialized
    __ jcc(Assembler::greater, loop);
    __ bind(exit);
  }
 
  // 生成固定楨
  // initialize fixed part of activation frame
  generate_fixed_frame(false);
 
  // 省略統計及棧溢位等邏輯,後面會詳細介紹
 
  // check for synchronized methods
  // Must happen AFTER invocation_counter check and stack overflow check,
  // so method is not locked if overflows.
  if (synchronized) {
    // Allocate monitor and lock method
    lock_method();
  } else {
    // no synchronization necessary
  }

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

要對偏移的計算進行研究,如下:

// 當前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());

如果要列印這個方法生成的彙編程式碼,可以在方法的return語句之前新增如下2句列印程式碼:

address end =  __ pc();
Disassembler::decode(entry_point, end);

這樣,在執行Disassembler::decode()方法時,會將此方法生成的機器碼轉換為彙編列印到控制檯上。

呼叫generate_fixed_frame()方法之前生成的彙編程式碼如下:

Loaded disassembler from /home/mazhi/workspace/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/hsdis-amd64.so
[Disassembling for mach='i386:x86-64']
  0x00007fffe101e2e0: mov    0x10(%rbx),%rdx // 通過%rbx中儲存的Method*找到ConstMethod並儲存到%rdx
  0x00007fffe101e2e4: movzwl 0x2a(%rdx),%ecx // 通過ConstMethod*找到入引數量儲存在%ecx
  0x00007fffe101e2e8: movzwl 0x28(%rdx),%edx // 通過ConstMethod*找到本地變量表大小儲存在%edx
  0x00007fffe101e2ec: sub    %ecx,%edx       // 計算方法區域性變數可使用的本地變數空間的大小並儲存在%edx

  // ... 省略呼叫generate_stack_overflow_check()方法生成的彙編

  0x00007fffe101e43d: pop    %rax                   // 彈出返回地址
  0x00007fffe101e43e: lea    -0x8(%rsp,%rcx,8),%r14 // 計算第一個引數的地址
  // 為區域性變數slot(不包括方法入參)分配堆疊空間並初始化為0
  //  迴圈進行本地變量表空間的開闢
  // -- loop --
  0x00007fffe101e443: test   %edx,%edx  
  0x00007fffe101e445: jle    0x00007fffe101e454 // 由於%edx的大小等於0,所以不需要額外分配,直接跳轉到exit
  0x00007fffe101e44b: pushq  $0x0
  0x00007fffe101e450: dec    %edx
  0x00007fffe101e452: jg     0x00007fffe101e44b // 如果%edx的大小不等於0,跳轉到loop 

現在棧的狀態如下圖所示。

 

 現在r14指向區域性變數開始的位置,而argument和local variable都儲存在了局部變量表,rbp指向了局部變量表結束位置。現在各個暫存器的狀態如下: 

rax: return address  // %rax暫存器中儲存的是返回地址return address
rbx: Method*
r14: pointer to locals
r13: sender sp

在InterpreterGenerator::generate_normal_entry()函式中,接下來會以這樣的狀態呼叫generate_fixed_frame()函式來建立Java方法執行時所需要的棧幀。generate_fixed_frame()函式會在下一篇詳細介紹。

呼叫後棧幀變為如下的狀態:

上圖右邊的棧狀態隨著具體方法的不同會顯示不同的狀態,不過大概的狀態就是上圖所示的樣子。

呼叫完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陣列來儲存,這樣就可以使用方法型別做為陣列下標獲取對應的方法入口了。  

相關文章的連結如下:

1、在Ubuntu 16.04上編譯OpenJDK8的原始碼 

2、除錯HotSpot原始碼

3、HotSpot專案結構 

4、HotSpot的啟動過程 

5、HotSpot二分模型(1)

6、HotSpot的類模型(2)  

7、HotSpot的類模型(3) 

8、HotSpot的類模型(4)

9、HotSpot的物件模型(5)  

10、HotSpot的物件模型(6) 

11、操作控制代碼Handle(7)

12、控制代碼Handle的釋放(8)

13、類載入器 

14、類的雙親委派機制 

15、核心類的預裝載

16、Java主類的裝載  

17、觸發類的裝載  

18、類檔案介紹 

19、檔案流 

20、解析Class檔案 

21、常量池解析(1) 

22、常量池解析(2)

23、欄位解析(1)

24、欄位解析之偽共享(2) 

25、欄位解析(3)  

26、欄位解析之OopMapBlock(4)

27、方法解析之Method與ConstMethod介紹  

28、方法解析

29、klassVtable與klassItable類的介紹  

30、計算vtable的大小 

31、計算itable的大小 

32、解析Class檔案之建立InstanceKlass物件 

33、欄位解析之欄位注入 

34、類的連線  

35、類的連線之驗證 

36、類的連線之重寫(1) 

37、類的連線之重寫(2)

38、方法的連線  

39、初始化vtable 

40、初始化itable  

41、類的初始化 

42、物件的建立  

43、Java引用型別 

44、Java引用型別之軟引用(1)

45、Java引用型別之軟引用(2)

46、Java引用型別之弱引用與幻像引用  

47、Java引用型別之最終引用

48、HotSpot的垃圾回收演算法  

49、HotSpot的垃圾回收器

50、CallStub棧幀    

作者持續維護的個人部落格  classloading.com。

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

   

&n