第30篇-main()方法的執行


在第7篇詳細介紹過為Java方法創建的棧幀,如下圖所示。

調用完generate_fixed_frame()函數后一些寄存器中保存的值如下:

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

現在我們舉一個例子,來完整的走一下解釋執行的過程。這個例子如下:

package com.classloading;

public class Test {
	public static void main(String[] args) {
		int i = 0;
		i = i++;
	}
}

通過javap -verbose Test.class命令反編譯后的字節碼文件內容如下: 

Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // com/classloading/Test
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               main
   #9 = Utf8               ([Ljava/lang/String;)V
  #10 = Utf8               SourceFile
  #11 = Utf8               Test.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               com/classloading/Test
  #14 = Utf8               java/lang/Object
{
  ...

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: return
}

如上實例對應的棧幀狀態如下圖所示。

現在我們就以解釋執行的方式執行main()方法中的字節碼。由於是從虛擬機調用過來的,而調用完generate_fixed_frame()函數后一些寄存器中保存的值並沒有涉及到棧頂緩存,所以需要從iconst_0這個字節碼指令的vtos入口進入,然后找到iconst_0這個字節碼指令對應的機器指令片段。

現在回顧一下字節碼分派的邏輯,在generate_normal_entry()函數中會調用generate_fixed_frame()函數為Java方法的執行生成對應的棧幀,接下來還會調用dispatch_next()函數執行Java方法的字節碼,首次獲取字節碼時的匯編如下:

// 在generate_fixed_frame()方法中已經讓%r13存儲了bcp
movzbl 0x0(%r13),%ebx    // %ebx中存儲的是字節碼的操作碼
 
// $0x7ffff73ba4a0這個地址指向的是對應state狀態下的一維數組,長度為256
movabs $0x7ffff73ba4a0,%r10
 
// 注意%r10中存儲的是常量,根據計算公式%r10+%rbx*8來獲取指向存儲入口地址的地址,
// 通過*(%r10+%rbx*8)獲取到入口地址,然后跳轉到入口地址執行
jmpq *(%r10,%rbx,8)

注意如上的$0x7ffff73ba4a0這個常量值已經表示了棧頂緩存狀態為vtos下的一維數組首地址。而在首次進行方法的字節碼分派時,通過0x0(%r13)即可取出字節碼對應的Opcode,使用這個Opcode可定位到iconst_0的入口地址。

%r10指向的是對應棧頂緩存狀態state下的一維數組,長度為256,其中存儲的值為Opcode,這在第8篇詳細介紹過,示意圖如下圖所示。

 

現在就是看入口為vtos,出口為itos的iconst_0所要執行的匯編代碼了,如下:

...

// vtos入口
mov $0x1,%eax

...
// iconst_0對應的匯編代碼
xor    %eax,%eax

匯編指令足夠簡單,最后將值存儲到了%eax中,所以也就是棧頂緩存的出口狀態為itos。

上圖中綠色的部分是表達式棧,而紫色的部分是本地變量表,由於本地變量表的大小為2,所以我畫了2個slot。

執行下一個字節碼指令istore_1,所以也會執行字節碼分派相關的邏輯。這里需要提醒下,其實之前在介紹字節碼指令對應的匯編時,只關注去介紹了字節碼指令本身的執行邏輯,其實在為每個字節碼指令生成機器指令時,一般都會為這些字節碼指令生成3部分機器指令片段:

(1)不同棧頂狀態對應的入口執行邏輯;

(2)字節碼指令本身需要執行的邏輯;

(3)分派到下一個字節碼指令的邏輯。

對於字節碼指令模板定義中,如果flags中指令有disp,那么這些指令自己會含有分派的邏輯,如goto、ireturn、tableswitch、lookupswitch、jsr等。由於我們的指令是iconst_0,所以會為這個字節碼指令生成分派邏輯,這些生成的邏輯如下:

movzbl 0x1(%r13),%ebx    // %ebx中存儲的是字節碼的操作碼
 
movabs itos對應的一維數組的首地址,%r10

jmpq *(%r10,%rbx,8)

我們注意到了,如果要讓%ebx中存儲istore_1的Opcode,則%r13需要加上iconst_0指令的長度,即1。由於iconst_0執行后的出口棧頂緩存為itos,所以要找到入口狀態為itos,而Opcode為istore_1的機器指令片段執行。如下圖所示。

mov    %eax,-0x8(%r14)

代碼將棧頂的值%eax存儲到本地變量表下標索引為1的位置處。通過%r14很容易定位到本地變量表的位置,執行完成后的棧狀態如下圖所示。

執行iconst_0和istore_1時,整個過程沒有向表達式棧(上圖中sp/rsp開始以下的部分就是表達式棧)中壓入0,實際上如果沒有棧頂緩存的優化,應該將0壓入棧頂,然后彈出棧頂存儲到局部變量表,但是有了棧頂緩存后,沒有壓棧操作,也就有彈棧操作,所以能極大的提高程序的執行效率。

return指令判斷的邏輯比較多,主要是因為有些方法可能有synchronized關鍵字,所以會在方法棧中保存鎖相關的信息,而在return返回時,退棧要釋放鎖。不過我們現在只看針對本實例要運行的部分代碼,如下:

// 將JavaThread::do_not_unlock_if_synchronized屬性存儲到%dl中
0x00007fffe101b770: mov    0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized屬性值為false
0x00007fffe101b777: movb   $0x0,0x2ad(%r15)

// 將Method*加載到%rbx中
0x00007fffe101b77f: mov    -0x18(%rbp),%rbx
// 將Method::_access_flags加載到%ecx中
0x00007fffe101b783: mov    0x28(%rbx),%ecx
// 檢查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test   $0x20,%ecx
// 如果方法不是同步方法,跳轉到----unlocked----
0x00007fffe101b78c: je     0x00007fffe101b970

main()方法為非同步方法,所以跳轉到unlocked,在unlocked邏輯中會執行一些釋放鎖的邏輯,對於我們本實例來說這不重要,我們直接看退棧的操作,如下:

// 將-0x8(%rbp)處保存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov    -0x8(%rbp),%rbx

// 移除棧幀
// leave指令相當於:
//     mov %rbp, %rsp
//     pop %rbp
0x00007fffe101bacb: leaveq 
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop    %r13
// 設置%rsp為調用者的棧頂值
0x00007fffe101bace: mov    %rbx,%rsp
0x00007fffe101bad1: jmpq   *%r13

這個匯編不難,這里不再繼續介紹。退棧后的棧狀態如下圖所示。

這就完全回到了調用Java方法之前的棧狀態,接下來如何退出如上棧幀並結束方法調用就是C++語言的事兒了。 

推薦閱讀:

第1篇-關於JVM運行時,開篇說的簡單些

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

第3篇-CallStub新棧幀的創建

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

第5篇-調用Java方法后彈出棧幀及處理返回結果

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

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

第8篇-dispatch_next()函數分派字節碼

第9篇-字節碼指令的定義

第10篇-初始化模板表

第11篇-認識Stub與StubQueue

第12篇-認識CodeletMark

第13篇-通過InterpreterCodelet存儲機器指令片段

第14篇-生成重要的例程

第15章-解釋器及解釋器生成器

第16章-虛擬機中的匯編器

第17章-x86-64寄存器

第18章-x86指令集之常用指令

第19篇-加載與存儲指令(1)

第20篇-加載與存儲指令之ldc與_fast_aldc指令(2)

第21篇-加載與存儲指令之iload、_fast_iload等(3)

第22篇-虛擬機字節碼之運算指令

第23篇-虛擬機字節碼指令之類型轉換

第24篇-虛擬機對象操作指令之getstatic

第25篇-虛擬機對象操作指令之getfield

第26篇-虛擬機對象操作指令之putstatic

第27篇-虛擬機字節碼指令之操作數棧管理指令

第28篇-虛擬機字節碼指令之控制轉移指令

第29篇-調用Java主類的main()方法

 

 

  

 

 

 

 

 

  

 

  

 


免責聲明!

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



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