摘要:
文前說明
作為碼農中的一員,需要不斷的學習,我工作之餘將一些分析總結和學習筆記寫成部落格與大家一起交流,也希望採用這種方式記錄自己的學習之旅。
本文僅供學習交流使用,侵權必刪。
...
文前說明
作為碼農中的一員,需要不斷的學習,我工作之餘將一些分析總結和學習筆記寫成部落格與大家一起交流,也希望採用這種方式記錄自己的學習之旅。
本文僅供學習交流使用,侵權必刪。
不用於商業目的,轉載請註明出處。
1. 概述
-
Java 虛擬機器的指令由一個位元組
長度的、代表著某種特定操作含義的數字(稱為操作碼
Opcode)以及跟隨其後的零至多個代表此操作所需引數(稱為運算元
Operands)而構成。
- 由於 Java 虛擬機器採用面向運算元棧而不是暫存器的架構,所以大多數的指令都不包含運算元,只有一個操作碼。
-
由於 Java 位元組碼的操作碼的長度為一個位元組(即 0~255),這意味著指令集的操作碼總數不可能超過 256 條。
-
如果要求 Java 執行時所有的資料型別都有對應與之相關的指令去支援的話,操作碼的總數將超過 256 條。所以 Java 位元組碼指令集被設計為 Not Orthogonal(非完全獨立),即並非每種資料型別和每種操作都有對應的指令,一些指令可以在必要的時候將一些不被支援的資料型別轉換為被支援的資料型別。
- 在虛擬機器的指令集中,大多數的指令都包含了其操作所對應的資料型別資訊,如(iload(代表 int)、fload(代表 float)、lload(代表 long)、dload(代表 double)、aload(代表引用資料型別)等)。
- 通過轉換指令,將一些不支援的型別轉換成支援的型別,位元組碼指令本身不支援如 char、byte、short 等基本資料型別(即不包含這些基本資料型別的資訊),Java 解決方案是將這些不支援的基本資料型別當成 int 型別處理,減少了很多的指令。
-
當資料大小超過一個位元組時,Java 虛擬機器需要重構出具體資料的結構。(比如:將一個 16 位長度的無符號整數使用兩個無符號位元組(byte1,byte2)儲存起來,那它們的值應該是((byte1<<8)|byte2
),除了 long 和 double 型別外,每個變數都佔區域性變數區中的一個變數槽(slot),而 long 及 double 會佔用兩個連續的變數槽。
-
位元組碼指令集的優點。
- 放棄了運算元長度對齊,意味著可以節省很多填充和間隔符號。
- 用一個位元組代表操作碼可以獲得儘可能短小精幹的編譯程式碼。
-
位元組碼指令集的不足。
- 在解釋執行位元組碼時會損失一些效能。(以時間換空間)
2. 常用的位元組碼指令
2.1 載入和儲存指令
- 載入和儲存指令用於將資料在棧幀中的區域性變量表和運算元棧之間來回傳輸。(如果將進行運算等操作需要入棧,將區域性變量表中的資料入棧載入入運算元棧中,在運算元棧中運算完畢再將結果出棧儲存到區域性變量表中)
指令 |
說明 |
樣例 |
load 指令 |
將一個區域性變數載入到操作棧 |
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n> |
store 指令 |
將一個數值從運算元棧儲存到區域性變量表 |
istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n> |
push、dc、const 指令 |
將一個常量載入到運算元棧 |
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d> |
wide 指令 |
擴充區域性變量表的訪問索引 |
wide |
- 儲存資料的運算元棧和區域性變量表主要就是由載入和儲存指令進行操作,除此之外,還有少量指令,如訪問物件的欄位或陣列元素的指令也會向運算元棧傳輸資料。
load 系列指令
-
把本地變數的送到棧頂。這裡的本地變數不僅可以是數值型別,還可以是引用型別。
- 對於前四個本地變數可以採用 iload_0、iload_1、iload_2、iload_3(分別表示第 0、1、2、3 個整形變數)這種無參簡化命令形式。
- 對本地變數所進行的編號,是對所有型別的本地變數進行的(並不按照型別分類)。
- 對於非靜態函式,第一變數是 this,即對於其的操作是 aload_0。
- 函式的傳入引數也算本地變數,在進行編號時,它是先於函式體的本地變數的。
指令碼 |
助記符 |
說明 |
0x15 |
iload |
將指定的 int 型本地變數推送至棧頂。 |
0x16 |
lload |
將指定的 long 型本地變數推送至棧頂。 |
0x17 |
fload |
將指定的 float 型本地變數推送至棧頂。 |
0x18 |
dload |
將指定的 double 型本地變數推送至棧頂。 |
0x19 |
aload |
將指定的引用型別本地變數推送至棧頂。 |
0x1a |
iload_0 |
將第一個 int 型本地變數推送至棧頂。 |
0x1b |
iload_1 |
將第二個 int 型本地變數推送至棧頂。 |
0x1c |
iload_2 |
將第三個 int 型本地變數推送至棧頂。 |
0x1d |
iload_3 |
將第四個 int 型本地變數推送至棧頂。 |
0x1e |
lload_0 |
將第一個 long 型本地變數推送至棧頂。 |
0x1f |
lload_1 |
將第二個 long 型本地變數推送至棧頂。 |
0x20 |
lload_2 |
將第三個 long 型本地變數推送至棧頂。 |
0x21 |
lload_3 |
將第四個 long 型本地變數推送至棧頂。 |
0x22 |
fload_0 |
將第一個 float 型本地變數推送至棧頂。 |
0x23 |
fload_1 |
將第二個 float 型本地變數推送至棧頂。 |
0x24 |
fload_2 |
將第三個 float 型本地變數推送至棧頂。 |
0x25 |
fload_3 |
將第四個 float 型本地變數推送至棧頂。 |
0x26 |
dload_0 |
將第一個 double 型本地變數推送至棧頂。 |
0x27 |
dload_1 |
將第二個 double 型本地變數推送至棧頂。 |
0x28 |
dload_2 |
將第三個 double 型本地變數推送至棧頂。 |
0x29 |
dload_3 |
將第四個 double 型本地變數推送至棧頂。 |
0x2a |
aload_0 |
將第一個引用型別本地變數推送至棧頂。 |
0x2b |
aload_1 |
將第二個引用型別本地變數推送至棧頂。 |
0x2c |
aload_2 |
將第三個引用型別本地變數推送至棧頂。 |
0x2d |
aload_3 |
將第四個引用型別本地變數推送至棧頂。 |
store 系列指令
-
把棧頂的值存入本地變數。這裡的本地變數不僅可以是數值型別,還可以是引用型別。
- 對於前四個本地變數可以採用 istore_0、istore_1、istore_2、istore_3(分別表示第 0、1、2、3 個整形變數)這種無參簡化命令形式。
- 對本地變數所進行的編號,是對所有型別的本地變數進行的(並不按照型別分類)。
- 對於非靜態函式,第一變數是 this,它是隻讀的。
- 函式傳入引數也算本地變數,在進行編號時,它是先於函式體的本地變數的。
指令碼 |
助記符 |
說明 |
0x36 |
istore |
將棧頂 int 型數值存入指定本地變數。 |
0x37 |
lstore |
將棧頂 long 型數值存入指定本地變數。 |
0x38 |
fstore |
將棧頂 float 型數值存入指定本地變數。 |
0x39 |
dstore |
將棧頂 double 型數值存入指定本地變數。 |
0x3a |
astore |
將棧頂引用型數值存入指定本地變數。 |
0x3b |
istore_0 |
將棧頂 int 型數值存入第一個本地變數。 |
0x3c |
istore_1 |
將棧頂 int 型數值存入第二個本地變數。 |
0x3d |
istore_2 |
將棧頂 int 型數值存入第三個本地變數。 |
0x3e |
istore_3 |
將棧頂 int 型數值存入第四個本地變數。 |
0x3f |
lstore_0 |
將棧頂 long 型數值存入第一個本地變數。 |
0x40 |
lstore_1 |
將棧頂 long 型數值存入第二個本地變數。 |
0x41 |
lstore_2 |
將棧頂 long 型數值存入第三個本地變數。 |
0x42 |
lstore_3 |
將棧頂 long 型數值存入第四個本地變數。 |
0x43 |
fstore_0 |
將棧頂 float 型數值存入第一個本地變數。 |
0x44 |
fstore_1 |
將棧頂 float 型數值存入第二個本地變數。 |
0x45 |
fstore_2 |
將棧頂 float 型數值存入第三個本地變數。 |
0x46 |
fstore_3 |
將棧頂 float 型數值存入第四個本地變數。 |
0x47 |
dstore_0 |
將棧頂 double 型數值存入第一個本地變數。 |
0x48 |
dstore_1 |
將棧頂 double 型數值存入第二個本地變數。 |
0x49 |
dstore_2 |
將棧頂 double 型數值存入第三個本地變數。 |
0x4a |
dstore_3 |
將棧頂 double 型數值存入第四個本地變數。 |
0x4b |
astore_0 |
將棧頂引用型數值存入第一個本地變數。 |
0x4c |
astore_1 |
將棧頂引用型數值存入第二個本地變數。 |
0x4d |
astore_2 |
將棧頂引用型數值存入第三個本地變數。 |
0x4e |
astore_3 |
將棧頂引用型數值存入第四個本地變數。 |
push 系列指令
-
把一個整形數字(長度比較小)送到到棧頂。命令有一個引數,用於指定要送到棧頂的數字。
- 命令只能操作一定範圍內的整形數值,超出該範圍的使用將使用 ldc 命令。
指令碼 |
助記符 |
說明 |
0x10 |
bipush |
將單位元組的常量值(-128~127)推送至棧頂。 |
0x11 |
sipush |
將一個短整型常量值(-32768~32767)推送至棧頂。 |
dc 系列指令
-
把數值常量或 String 常量值從常量池中推送至棧頂。
- 該命令後面需要給一個表示常量在常量池中位置(編號)的引數。
- 對於 const 系列和 push 系列指令操作範圍之外的數值型別常量,都放在常量池中。
- 所有不是通過 new 建立的 String 都是放在常量池中的。
指令碼 |
助記符 |
說明 |
0x12 |
ldc |
將 int,float 或 String 型常量值從常量池中推送至棧頂。 |
0x13 |
ldc_w |
將 int,float 或 String 型常量值從常量池中推送至棧頂(寬索引)。 |
0x14 |
ldc2_w |
將 long 或 double 型常量值從常量池中推送至棧頂(寬索引)。 |
const 系列指令
指令碼 |
助記符 |
說明 |
0x02 |
iconst_m1 |
將 int 型(-1)推送至棧頂。 |
0x03 |
iconst_0 |
將 int 型(0)推送至棧頂。 |
0x04 |
iconst_1 |
將 int 型(1)推送至棧頂。 |
0x05 |
iconst_2 |
將 int 型(2)推送至棧頂。 |
0x06 |
iconst_3 |
將 int 型(3)推送至棧頂。 |
0x07 |
iconst_4 |
將 int 型(4)推送至棧頂。 |
0x08 |
iconst_5 |
將 int 型(5)推送至棧頂。 |
0x09 |
lconst_0 |
將 long 型(0)推送至棧頂。 |
0x0a |
lconst_1 |
將 long 型(1)推送至棧頂。 |
0x0b |
fconst_0 |
將 float 型(0)推送至棧頂。 |
0x0c |
fconst_1 |
將 float 型(1)推送至棧頂。 |
0x0d |
fconst_2 |
將 float 型(2)推送至棧頂。 |
0x0e |
dconst_0 |
將 double 型(0)推送至棧頂。 |
0x0f |
dconst_1 |
將 double 型(1)推送至棧頂。 |
2.2 運算指令
-
運算或算術指令用於對兩個運算元棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。
- 算術指令分為兩種:整型運算的指令和浮點型運算的指令。
- 無論是哪種算術指令,都使用 Java 虛擬機器的資料型別,由於沒有直接支援 byte、short、char 和 boolean 型別的算術指令,使用操作 int 型別的指令代替。
指令 |
樣例 |
加法指令 |
iadd、ladd、fadd、dadd。 |
減法指令 |
isub、lsub、fsub、dsub。 |
乘法指令 |
imul、lmul、fmul、dmul。 |
除法指令 |
idiv、ldiv、fdiv、ddiv。 |
求餘指令 |
irem、lrem、frem、drem。 |
取反指令 |
ineg、lneg、fneg、dneg。 |
位移指令 |
ishl、ishr、iushr、lshl、lshr、lushr。 |
按位或指令 |
ior、lor。 |
按位與指令 |
iand、land。 |
按位異或指令 |
ixor、lxor。 |
區域性變數自增指令 |
iinc。 |
比較指令 |
dcmpg、dcmpl、fcmpg、fcmpl、lcmp。 |
指令碼 |
助記符 |
說明 |
0x5f |
swap |
將棧最頂端的兩個數值互換(數值不能是 long 或 double 型別的)。 |
0x60 |
iadd |
將棧頂兩 int 型數值相加並將結果壓入棧頂。 |
0x61 |
ladd |
將棧頂兩 long 型數值相加並將結果壓入棧頂。 |
0x62 |
fadd |
將棧頂兩 float 型數值相加並將結果壓入棧頂。 |
0x63 |
dadd |
將棧頂兩 double 型數值相加並將結果壓入棧頂。 |
0x64 |
isub |
將棧頂兩 int 型數值相減並將結果壓入棧頂。 |
0x65 |
lsub |
將棧頂兩 long 型數值相減並將結果壓入棧頂。 |
0x66 |
fsub |
將棧頂兩 float 型數值相減並將結果壓入棧頂。 |
0x67 |
dsub |
將棧頂兩 double 型數值相減並將結果壓入棧頂。 |
0x68 |
imul |
將棧頂兩 int 型數值相乘並將結果壓入棧頂。 |
0x69 |
lmul |
將棧頂兩 long 型數值相乘並將結果壓入棧頂。 |
0x6a |
fmul |
將棧頂兩 float 型數值相乘並將結果壓入棧頂。 |
0x6b |
dmul |
將棧頂兩 double 型數值相乘並將結果壓入棧頂。 |
0x6c |
idiv |
將棧頂兩 int 型數值相除並將結果壓入棧頂。 |
0x6d |
ldiv |
將棧頂兩 long 型數值相除並將結果壓入棧頂。 |
0x6e |
fdiv |
將棧頂兩 float 型數值相除並將結果壓入棧頂。 |
0x6f |
ddiv |
將棧頂兩 double 型數值相除並將結果壓入棧頂。 |
0x70 |
irem |
將棧頂兩 int 型數值作取模運算並將結果壓入棧頂。 |
0x71 |
lrem |
將棧頂兩 long 型數值作取模運算並將結果壓入棧頂。 |
0x72 |
frem |
將棧頂兩 float 型數值作取模運算並將結果壓入棧頂。 |
0x73 |
drem |
將棧頂兩 double 型數值作取模運算並將結果壓入棧頂。 |
0x74 |
ineg |
將棧頂 int 型數值取負並將結果壓入棧頂。 |
0x75 |
lneg |
將棧頂 long 型數值取負並將結果壓入棧頂。 |
0x76 |
fneg |
將棧頂 float 型數值取負並將結果壓入棧頂。 |
0x77 |
dneg |
將棧頂 double 型數值取負並將結果壓入棧頂。 |
0x78 |
ishl |
將 int 型數值左移位指定位數並將結果壓入棧頂。 |
0x79 |
lshl |
將 long 型數值左移位指定位數並將結果壓入棧頂。 |
0x7a |
ishr |
將 int 型數值右(符號)移位指定位數並將結果壓入棧頂。 |
0x7b |
lshr |
將 long 型數值右(符號)移位指定位數並將結果壓入棧頂。 |
0x7c |
iushr |
將 int 型數值右(無符號)移位指定位數並將結果壓入棧頂。 |
0x7d |
lushr |
將 long 型數值右(無符號)移位指定位數並將結果壓入棧頂。 |
0x7e |
iand |
將棧頂兩 int 型數值作 " 按位與 " 並將結果壓入棧頂。 |
0x7f |
land |
將棧頂兩 long 型數值作 " 按位與 " 並將結果壓入棧頂。 |
0x80 |
ior |
將棧頂兩 int 型數值作 " 按位或 " 並將結果壓入棧頂。 |
0x81 |
lor |
將棧頂兩 long 型數值作 " 按位或 " 並將結果壓入棧頂。 |
0x82 |
ixor |
將棧頂兩 int 型數值作 " 按位異或 " 並將結果壓入棧頂。 |
0x83 |
lxor |
將棧頂兩 long 型數值作 " 按位異或 " 並將結果壓入棧頂。 |
-
異常情況:僅規定了在處理整型資料時只有除法指令(idiv、ldiv)以及求餘指令(irem、lrem)中出現除數為零時會導致虛擬機器丟擲 ArithmeticException 異常
,其餘任何整型數運算場景都不應該丟擲執行時異常。
- 當一個操作產生溢位時,將會使用有符號的無窮大來表示(NaN)。
- 在對 long 型別的資料進行比較時,虛擬機器採用帶符號的比較方式,而在對浮點數值精心比較時,虛擬機器採用 IEEE 754 規範所定義的無訊號比較方式。
2.3 型別轉換指令
- 型別轉換指令可以將兩種不同的數值型別進行相互轉換。
- 轉換操作一般用於實現使用者程式碼中的顯式型別轉換操作,或者用來處理位元組碼指令集中資料型別相關指令無法與資料型別一一對應的問題。
寬化型別轉換
-
Java 虛擬機器天然支援基本資料型別的寬化型別轉換。
- int 型別到 long、float 或者 double 型別(i2l、i2f、i2d)。
- long 型別到 float、double 型別(l2f、l2d)。
- float 型別到 double 型別(f2d)。
窄化型別轉換
-
窄化型別轉換必須顯式地使用轉換指令來完成。
-
可能會導致轉換結果產生不同的正負號、不同的數量級的情況,轉化過程很可能會導致數值精度的丟失。直接丟棄多出來的高位
。
-
將一個浮點數窄化轉換成整數型別 T(T 限於 int 或 long 型別之一)的時候,將遵循以下轉換規則。
- 如果浮點值是 NaN,那轉換結果就是 int 或 long 中的 0。
-
如果浮點值不是無窮大的話,浮點值使用向零舍入模式取整
,獲取整數值 v,如果 v 在目標 T 的表示範圍內,那麼結果就是 T;否則將根據 v 的符號,轉換為 T 所能表示的最大或最小整數。
-
Java 虛擬機器規範中明確規定數值型別的窄化轉換指令永遠不可能導致虛擬機器丟擲執行時異常
。
- 常見的轉換指令有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f 等。
2.4 物件建立與訪問指令
-
對於普通物件和陣列的建立,Java 虛擬機器分別使用了不同的指令去處理。
- 建立類例項的指令 new。
- 建立陣列的指令 newarray、anewarray、multianewarray。
- 訪問類變數(static 欄位)和例項變數(非 static 欄位)的指令 getfield、putfield、getstatic、putstatic。
- 把一個數組元素載入到運算元棧 baload、caload、saload、iaload、laload、faload、daload、aaload。
- 將一個運算元棧的值儲存到陣列元素中的指令 bastore、castore、sastore、iastore、fastore、dastore、aastore。
- 取陣列長度的指令 arraylength。
- 檢查普通物件型別的指令 instanceof、checkcast。
- 把陣列的某項送到棧頂。該命令根據棧裡內容來確定對哪個陣列的哪項進行操作。
指令碼 |
助記符 |
說明 |
0x2e |
iaload |
將 int 型陣列指定索引的值推送至棧頂。 |
0x2f |
laload |
將 long 型陣列指定索引的值推送至棧頂。 |
0x30 |
faload |
將 float 型陣列指定索引的值推送至棧頂。 |
0x31 |
daload |
將 double 型陣列指定索引的值推送至棧頂。 |
0x32 |
aaload |
將引用型陣列指定索引的值推送至棧頂。 |
0x33 |
baload |
將 boolean 或 byte 型陣列指定索引的值推送至棧頂。 |
0x34 |
caload |
將 char 型陣列指定索引的值推送至棧頂。 |
0x35 |
saload |
將 short 型陣列指定索引的值推送至棧頂。 |
- 把棧頂項的值存到數組裡。該命令根據棧裡內容來確定對哪個陣列的哪項進行操作。
指令碼 |
助記符 |
說明 |
0x4f |
iastore |
將棧頂 int 型數值存入指定陣列的指定索引位置。 |
0x50 |
lastore |
將棧頂 long 型數值存入指定陣列的指定索引位置。 |
0x51 |
fastore |
將棧頂 float 型數值存入指定陣列的指定索引位置。 |
0x52 |
dastore |
將棧頂 double 型數值存入指定陣列的指定索引位置。 |
0x53 |
aastore |
將棧頂引用型數值存入指定陣列的指定索引位置。 |
0x54 |
bastore |
將棧頂 boolean 或 byte 型數值存入指定陣列的指定索引位置。 |
0x55 |
castore |
將棧頂 char 型數值存入指定陣列的指定索引位置。 |
0x56 |
sastore |
將棧頂 short 型數值存入指定陣列的指定索引位置。 |
2.5 運算元棧管理指令
- 如同操作一個普通資料結構中的堆疊那樣,Java 虛擬機器提供了一些用於直接操作運算元棧的指令。
指令碼 |
助記符 |
說明 |
0x57 |
pop |
將棧頂數值彈出(數值不能是 long 或 double 型別的)。 |
0x58 |
pop2 |
將棧頂的一個(long 或 double 型別的)或兩個數值彈出(其它)。 |
0x59 |
dup |
複製棧頂數值(數值不能是 long 或 double 型別的)並將複製值壓入棧頂。 |
0x5a |
dup_x1 |
複製棧頂數值(數值不能是 long 或 double 型別的)並將兩個複製值壓入棧頂。 |
0x5b |
dup_x2 |
複製棧頂數值(數值不能是 long 或 double 型別的)並將三個(或兩個)複製值壓入棧頂。 |
0x5c |
dup2 |
複製棧頂一個(long 或 double 型別的)或兩個(其它)數值並將複製值壓入棧頂。 |
0x5d |
dup2_x1 |
複製棧頂數值(long 或 double 型別的)並將兩個複製值壓入棧頂。 |
0x5e |
dup2_x2 |
複製棧頂數值(long 或 double 型別的)並將三個(或兩個)複製值壓入棧頂。 |
2.6 控制轉移指令
-
控制轉移指令可以讓 Java 虛擬機器有條件或無條件地從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程式。
-
從概念模型上理解,可以認為控制轉移指令就是在有條件或無條件地修改 PC 暫存器的值。
-
條件分支
ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
-
複合條件分支
tableswitch、lookupswitch。
-
無條件分支
goto、goto_w、jsr、jsr_w、ret。
- 在 Java 虛擬機器中有專門的指令集用來處理 int 和 reference 型別的條件分支比較操作,為了可以無須明顯標識一個實體值是否 null,也有專門的指令用來檢測 null 值。
2.7 方法呼叫和返回指令
方法呼叫指令
-
前 4 條呼叫指令的分派邏輯都固化在 Java 虛擬機器內部,而 invokedynamic 指令的分派邏輯是由使用者所設定的引導方法決定的。
- invokevirtual 指令用於呼叫物件的例項方法,根據物件的實際型別進行分派(虛方法分派),這也是 Java 語言中最常見的方法分派方式。
- invokeinterface 指令用於呼叫介面方法,會在執行時搜尋一個實現了這個介面方法的物件,找出適合的方法進行呼叫。
- invokespecial 指令用於呼叫一些需要特殊處理的例項方法,包括例項初始化(<init>)方法、私有方法和父類方法。
- invokestatic 呼叫靜態方法(static 方法)。
- invokedynamic 指令用於在執行時動態解析出呼叫點限定符所引用的方法,並執行該方法。
方法返回指令
- 方法返回指令根據返回值的型別進行區分,包括 ireturn(返回值為 boolean、byte、char、short 和 int 型別時使用)、lreturn、freturn、dreturn 和 areturn,另外還有一條 return 指令供宣告為 void 的方法、例項初始化方法 <init> 以及類和介面的類初始化方法 <clinit> 使用。
關於方法呼叫
- Class 檔案的編譯過程中不包含傳統編譯中的連線步驟,所有方法呼叫中的目標方法在 Class 檔案裡面都是一個常量池中的符號引用,而不是方法在實際執行時記憶體佈局中的入口地址。
-
在類載入的解析階段,會將其中的一部分符號引用轉化為直接引用,這類方法(編譯期可知,執行期不可變)的呼叫稱為解析(Resolution)。
- 主要包括靜態方法和私有方法兩大類,前者與型別直接關聯,後者在外部不可被訪問,這兩種方法各自的特點決定了它們都不可能通過繼承或別的方式重寫其他版本,因此它們都適合在類載入階段進行解析。
- 只要能被 invokestatic 和 invokespecial 指令呼叫的方法,都可以在解析階段中確定唯一的呼叫版本,符合這個條件的有靜態方法、私有方法、例項構造器、父類方法 4 類,它們在類載入的時候就會把符號引用解析為該方法的直接引用。
- 動態型別語言的關鍵特徵是它的型別檢查的主體過程是在執行期而不是編譯期。
2.8 異常處理指令
-
在 Java 程式中顯式丟擲異常的操作(throw 語句)都由 athrow 指令實現,除了用 throw 語句顯式丟擲異常情況之外,Java 虛擬機器規範還規定了許多執行時異常會在其他 Java 虛擬機器指令檢測到異常狀況時自動丟擲。
- 例如在整數運算中,當除數為零時,虛擬機器會在 idiv 或 ldiv 指令中丟擲 ArithmeticException 異常。
-
在 Java 虛擬機器中,處理異常(catch 語句)不是由位元組碼指令來實現的(很久之前曾經使用 jsr 和 ret 指令來實現,現在已經不用了),而是採用異常表
來完成的。
2.9 同步指令
- Java 虛擬機器可以支援方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支援的。
方法級同步
-
方法級的同步:隱式的,即無需通過位元組碼指令控制,它實現在方法呼叫和返回操作之中。
- 當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCRONIZED 訪問標誌是否被設定,如果是,執行程式就要求先成功持有管程,然後才能執行方法,最後當方法執行完成時(無論是正常完成還是非正常完成)時釋放管程。
- 在方法執行期間,執行執行緒持有了管程,其他任何執行緒都無法再獲取到同一個管程。
- 如果執行期間出現了方法內部無法解決的異常,那麼這個方法所持有的管程將在異常丟擲到同步方法之外時自動釋放。
方法內部一段指令序列的同步
-
同步一段指令集序列通常是由 Java 語言中的synchronized 語句塊來表示的,Java 虛擬機器的指令集中有 monitorenter 和 monitorexit 兩條指令來支援 synchronized 關鍵字的語義,正確實現 synchronized 關鍵字需要 Javac 編譯器與 Java 虛擬機器兩者共同協作支援。
- 編譯器必須保證無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter 指令都必須執行其對應的 monitorexit 指令,而無論這個方法時正常結束還是異常結束。
2.10 程式執行使用樣例
public class Test {
public static int minus(int x) {
return -x;
}
public static void main(String[] args) {
int x = 5;
int y = minus(x);
}
}
- 從固化在 Class 檔案中的二進位制位元組碼開始,經過載入器對當前類的載入,虛擬機器對二進位制碼的驗證、準備和一定的解析,進入記憶體中的方法區,常量池中的符號引用一定程度上轉換為直接引用,使得位元組碼通過結構化的組織讓虛擬機器瞭解類的每一塊的構成,建立的執行緒申請到了虛擬機器棧中的空間構造出屬於這一執行緒的棧幀空間。
{
public services.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1// Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
StartLengthSlotNameSignature
050thisLservices/Test;
public static int minus(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: ineg
2: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
StartLengthSlotNameSignature
030xI
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_5
1: istore_1
2: iload_1
3: invokestatic#2// Method minus:(I)I
6: istore_2
7: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 7
LocalVariableTable:
StartLengthSlotNameSignature
080args[Ljava/lang/String;
261xI
712yI
}
-
檢查
main()
方法的訪問標誌(ACC_PUBLIC,ACC_STATIC)、描述符描述的返回型別和引數列表,確定可以訪問後進入 Code 屬性表執行命令,讀入棧深度建立符合要求的運算元棧,讀入區域性變數大小建立符合要求的區域性變量表,根據引數數向區域性變量表中依序加入引數(第一個引數是引用當前物件的 this,所以空引數列表的引數數量也是 1)。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_5
1: istore_1
2: iload_1
3: invokestatic#2// Method minus:(I)I
6: istore_2
7: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 7
LocalVariableTable:
StartLengthSlotNameSignature
080args[Ljava/lang/String;
261xI
712yI
0: iconst_5
- 將棧頂整數值存入區域性變量表的 slot1(slot0 是引數 this)。
1: istore_1
2: iload_1
-
invokestatic 指令用於呼叫靜態方法,引數是根據常量池中已經轉換為直接引用的常量,即
minus()
函式在方法區中的地址,找到這個地址呼叫函式,向其中加入的引數為棧頂的值。
3: invokestatic#2// Method minus:(I)I
6: istore_2
- 將返回地址中儲存的 PC 地址返到 PC,棧幀恢復到呼叫前。
7: return
-
minus()
函式執行過程,同樣的首先檢查函式的訪問標誌、描述符描述的返回型別和引數列表,確定可以訪問後進入 Code 屬性表執行命令,讀入棧深度建立符合要求的運算元棧,讀入區域性變數大小建立符合要求的區域性變量表,根據引數數向區域性變量表中依序加入引數,然後開始根據命令正式執行。
public static int minus(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: ineg
2: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
StartLengthSlotNameSignature
030xI
0: iload_0
1: ineg
- 將返回地址中儲存的 PC 地址返到 PC,棧幀恢復到呼叫前。
2: ireturn
-
從二進位制位元組碼裡可以看到 invokestatic 指令呼叫的是
minus()
方法的直接引用,在編譯期這個呼叫就已經決定了。如果方法是動態繫結,在編譯期並不知道使用哪個方法(或者是不知道使用方法的哪個版本),那麼這個時候就需要在執行時才能確定哪個版本的方法將被呼叫,這個時候才能將符號引用轉換為直接引用。這個問題提到的多個版本的方法與 Java 中的過載
和多型重寫
問題息息相關。
過載(override)
public class Test {
static class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human human) {
System.out.println("hello human");
}
public void sayHello(Man man) {
System.out.println("hello man");
}
public void sayHello(Woman woman) {
System.out.println("hello woman");
}
public static void main(String[] args) {
Test demo = new Test();
Human man = new Man();
Human woman = new Woman();
demo.sayHello(man);
demo.sayHello(woman);
}
}
/*print
hello human
hello human
*/
-
在過載中,程式呼叫的是引數實際型別不同的方法,但是虛擬機器最終分派了相同外觀型別(靜態型別)的方法,這說明在過載的過程中虛擬機器在執行的時候是隻看引數的外觀型別(靜態型別)的,而這個外觀型別(靜態型別)是在編譯的時候就已經確定,和虛擬機器沒有關係。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new#7// class services/Test
3: dup
4: invokespecial #8// Method "<init>":()V
7: astore_1
8: new#9// class services/Test$Man
11: dup
12: invokespecial #10// Method services/Test$Man."<init>":()V
15: astore_2
16: new#11// class services/Test$Woman
19: dup
20: invokespecial #12// Method services/Test$Woman."<init>":()V
23: astore_3
24: aload_1
25: aload_2
26: invokevirtual #13// Method sayHello:(Lservices/Test$Human;)V
29: aload_1
30: aload_3
31: invokevirtual #13// Method sayHello:(Lservices/Test$Human;)V
34: return
LineNumberTable:
line 29: 0
line 30: 8
line 31: 16
line 32: 24
line 33: 29
line 34: 34
LocalVariableTable:
StartLengthSlotNameSignature
0350args[Ljava/lang/String;
8271demoLservices/Test;
16192manLservices/Test$Human;
24113 womanLservices/Test$Human;
重寫(overwrite)
public class Test {
static class Human {
public void sayHello() {
System.out.println("hello human");
}
}
static class Man extends Human {
public void sayHello() {
System.out.println("hello man");
}
}
static class Woman extends Human {
public void sayHello() {
System.out.println("hello woman");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
/*print
hello man
hello woman
*/
-
在重寫中,程式呼叫的是不同實際型別的同名方法,虛擬機器依據物件的實際型別去尋找是否有這個方法,如果有就執行,如果沒有去父類裡找,最終在實際型別裡找到了這個方法,所以最終是在執行期動態分派了方法。
- 在編譯的時候可以看到位元組碼指示的方法都是一樣的符號引用,但是執行期虛擬機器能夠根據實際型別去確定出真正需要的直接引用。
-
這種依賴實際型別來做方法的分配叫做動態分派
。
- 得益於 Java 虛擬機器的動態分派會在分派前確定物件的實際型別,面向物件的多型性才能體現出來。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new#2// class services/Test$Man
3: dup
4: invokespecial #3// Method services/Test$Man."<init>":()V
7: astore_1
8: new#4// class services/Test$Woman
11: dup
12: invokespecial #5// Method services/Test$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6// Method services/Test$Human.sayHello:()V
20: aload_2
21: invokevirtual #6// Method services/Test$Human.sayHello:()V
24: return
LineNumberTable:
line 24: 0
line 25: 8
line 26: 16
line 27: 20
line 28: 24
LocalVariableTable:
StartLengthSlotNameSignature
0250args[Ljava/lang/String;
8171manLservices/Test$Human;
1692 womanLservices/Test$Human;
參考資料