您好,登錄后才能下訂單哦!
這是由一個“無聊”的問題引發的故事:方法ipp和ppi分別會打印什么結果?
public class Opcode { public static void main(String[] args) { System.out.println("hello wang ni ma"); } public void ipp(){ int i = 0; i = i++; System.out.println(i); } public void ppi(){ int i = 0; i = ++i; System.out.println(i); } }
當然了,把兩個方法放在一起,憑借些許的邏輯思維分析,可以很快給出答案: 0 1
那JVM為什么會執行出這樣的結果呢,本文將結合 字節碼 和 虛擬機棧 做出解釋。
javap是JDK自帶的反匯編器,可以查看java編譯器為我們生成的字節碼。通過它,我們可以對照源代碼和字節碼,從而了解很多編譯器內部的工作。
Java 程序編譯之后就變成了一條條字節碼指令,其形式類似匯編,但和匯編有不同之處:
匯編指令的操作數存放在數據段和寄存器中,可通過存儲器或寄存器尋址找到需要的操作數;
Java 字節碼指令的操作數存放在操作數棧中(可以理解為JVM內部虛擬寄存器),當執行某條帶 n 個操作數的指令時,就從棧頂取 n 個操作數,然后把指令的計算結果(如果有的話)入棧。
由于操作數棧是內存空間,所以字節碼指令不必擔心不同機器上寄存器以及機器指令的差別,從而做到了平臺無關。
Java虛擬機的指令由一個字節長度的、代表著某種特定操作含義的操作碼(Opcode)以及跟隨其后的零至多個代表此操作所需參數的操作數(Operands)所構成。
列舉本文用到的基本指令:
加載和存儲指令
將一個局部變量加載到操作棧:iload_<n>
將一個數值從操作數棧存儲到局部變量表:istore_<n>
將一個常量加載到操作數棧:iconst_<i>
運算指令 (運算之后的結果會自動入棧)
局部變量自增指令:iinc
對于大部分與數據類型相關的字節碼指令,他們的操作碼助記符中都有特殊的字符來表明專門為哪種數據類型服務:
i -- int
l -- long
s -- short
b -- byte
c -- char
f -- float
d -- double
a -- reference
我們用javac編譯上面的Opcode.java,然后“javap -c”查看字節碼:
javap命令加入“-v”可以看到更詳細的信息(常量池) :
在看圖之前我們先了解幾個概念:
Java虛擬機(JVM)是基于棧結構的,其中的“棧”指的就是操作數棧。
在代碼的實際運行中,每個線程都會創建一個JVM棧存儲棧幀(frame)。每當有方法調用時,frame就會被創建;當這個方法返回時,frame出棧。
一個frame由三部分組成:操作數棧(Oprand Stack)、局部變量表(Local Variable Table)、當前方法所在類的運行常量池的引用(The Reference of Constant Pool)。
局部變量表,存儲的是方法的參數和局部變量的值。存儲參數的索引從0開始,如果是構造方法或者實例化方法的frame,那么局部變量數組的“0”處存儲的是“this”引用,然后再從“1”開始存儲形參和局部變量;如果是靜態方法的frame,局部變量表不會存儲“this”引用,而是從“0”開始存儲形參和局部變量。
操作數棧,臨時存儲參與運算的數值,然后進行相關操作。和局部變量表一樣,操作數棧也是一個以字長為單位的數組。但是和前者不同的是,它不是通過索引來訪問,而是通過標準的棧操作壓棧/出棧來訪問的。
常量池,存儲在JVM內存線程共享區的“方法區”,在類初始化的時候,會為給出的常量分配一個常量池,并且為每一個常量給出引用。
ipp()
把常量“0”加載到操作數棧,指令“iconst_0”中的“0”代表int常量“0”
操作數棧:[0]
局部變量表:[this]
“i = i++;”進行了四次操作:
1 “istore_1”將操作數棧中棧頂的int壓入局部變量表“1”的位置
操作數棧:[]
局部變量表:[this, 0]
2 “iload_1”將局部變量表“1”處的int加載到操作數棧
操作數棧:[0]
局部變量表:[this, 0]
3 “iinc 1, 1”將局部變量表“1”處的int做自增運算,結果自動入棧
操作數棧:[0]
局部變量表:[this, 1]
4 “istore_1”將操作數棧“1”處的int壓入局部變量表
操作數棧:[]
局部變量表:[this, 0]
“System.out.println(i);”進行了三次操作:
1 “getstatic #2”指向常量池中的第2個位置,載入“System.out”域
2 “iload_1”將局部變量表“1”處的int加載到操作數棧
操作數棧:[0]
局部變量表:[this, 0]
3 “invokevirtul #5”指向常量池中的第5個位置,”調用實例方法“println”打印操作數棧的數值“0”
至此,ipp()方法的分析完成了,理解之后,反觀ppi()方法的字節碼信息,有一處不同:
相當于“i = i++”操作是先加載了局部變量表中的“0”到操作數棧,然后在局部變量表中做自增運算;而“i = ++i”是先在局部變量表中做自增運算,此時的值已經變成“1”,然后再把局部變量表中的“1”加載到操作數棧。這也就印證了坊間流傳的“i++是先賦值后運算,++i是先運算后賦值”這一說法。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。