在第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++語言的事兒了。
推薦閱讀:
第2篇-JVM虛擬機這樣來調用Java主類的main()方法
第13篇-通過InterpreterCodelet存儲機器指令片段
第20篇-加載與存儲指令之ldc與_fast_aldc指令(2)
第21篇-加載與存儲指令之iload、_fast_iload等(3)