Java 字節(jié)碼
Java 字節(jié)碼是 JVM 里面指令的型式, Java 的源碼經(jīng)過 Java 編譯器會(huì)形成 Java 字節(jié)碼,這的字節(jié)碼才能在 Java 虛擬機(jī)中運(yùn)行伏伐。
一宠进、椀唐鳎基架構(gòu)
一個(gè)虛擬機(jī)有基于棧虛擬機(jī)(Stack based Virtual Machine)和 基于寄存器虛擬機(jī)(Register based Virtual Machine)之法, 它們的差別可以看這里掂为。
Java 的虛擬機(jī)是基于棧的, 它包含 PC 寄存器欲诺、 JVM 棧毅厚、堆和方法區(qū)
注:圖片來自于 極客時(shí)間專欄《深入拆解 Java 虛擬機(jī)》
PC 寄存器(program counter): Java 程序里面的每一個(gè)運(yùn)行的線程伴网,都有一個(gè) PC 寄存器存儲(chǔ)著當(dāng)前指令的地址动分。
JVM 棧:對(duì)于每一個(gè)線程纠脾,棧 是用來存放局部變量、方法參數(shù)以及返回值的
堆:所有線程共享的內(nèi)存區(qū)域,存放著對(duì)象(類的實(shí)例化和數(shù)組)。對(duì)象由垃圾回收器進(jìn)行再分配
方法區(qū):對(duì)于每一個(gè)加載的類,方法區(qū)里面都存放著方法的代碼茶行,以及符號(hào)表(對(duì)象或字段的引用)以及常量池中的常量
JVM 的棧是由一系列的 Frame 組成的看锉,它包含
注:圖片來自JVM 內(nèi)部原理(六)— Java 字節(jié)碼基礎(chǔ)之一
一個(gè)局部變量數(shù)組(Local Variables), 索引從 0 到數(shù)組長(zhǎng)度減一趾诗。數(shù)據(jù)的長(zhǎng)度由編譯器計(jì)算。除了 long 和 double 類型值需要兩個(gè)局部變量的空間存儲(chǔ)外,其他類型的值都可以存儲(chǔ)在一個(gè)局部變量里;
一個(gè)用來存儲(chǔ)中間變量的操作棧(Operand Stack)。該中間變量的作用充當(dāng)指令的操作數(shù),或者存放方法調(diào)用的參數(shù)。它是一個(gè)后進(jìn)先出(LIFO)棧
二、JVM 數(shù)據(jù)類型
JVM 定義的數(shù)據(jù)類型有:
- 基本數(shù)據(jù)類型:
- 數(shù)字類型
類型 | 位數(shù) |
---|---|
byte | 8位 |
short | 16位 |
int | 32 位 |
long | 64 位 |
char | 16 位 |
float | 32 位 單精度 |
double | 64 位 雙精度 |
boolean 類型
returenAdress: 指令指針默蚌,指向一條虛擬機(jī)指令的操作碼
-
引用類型
- 類類型 class type
- 集合類型 array type
- 接口類型 interface type
三、指令
Java 字節(jié)碼的指令有一個(gè) JVM 數(shù)據(jù)類型前綴和操作名組成,例如 idd 指令是由 "i" 表示 int 類型數(shù)據(jù)和 “add"相加操作組成,因此 iadd 表示對(duì) int 類型數(shù)值求和瞳秽。
根據(jù)指令的性質(zhì),可以分為以下類型:
- 加載和存儲(chǔ)指令
- 運(yùn)算指令
- 轉(zhuǎn)移指令
- 對(duì)象創(chuàng)建和訪問指令
- 操作數(shù)棧管理指令
- 控制轉(zhuǎn)移指令
- 方法調(diào)用和返回指令
- 異常處理指令
- 同步指令
下面有詳細(xì)的說明
1.加載和存儲(chǔ)指令
加載和存儲(chǔ)指令用于數(shù)據(jù)在棧幀(Frame)中的局部變量數(shù)組(Local Variables)和操作棧(Operand Stack)之間來回傳輸连霉;
具體為:
- 將一個(gè)局部變量加載到操作棧: iload, xxxload
- 將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表: istore, xxxstore
- 將一個(gè)常量加載到操作數(shù)棧:bipush, xxxpush, iconst_xxx, ladc_w, ladc2_w, aconst_null 等
- 擴(kuò)充局部變量表的訪問索引的指令: wide
例如:
例子1:
Calc.java 文件
public class Calc {
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a + b ) * c;
}
}
通過命令
javac Calc.java
會(huì)生成 Calc.class 文件
然后在通過命令 javap 可以看到相應(yīng)的字節(jié)碼
javap -v Calc.class
相應(yīng)的字節(jié)碼
public int calc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: sipush 300
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: imul
16: ireturn
執(zhí)行過程
注:圖片來自于周志明《深入理解 Java 虛擬機(jī)》
2.運(yùn)算指令
運(yùn)算指令用于對(duì)兩個(gè)操作棧上的值進(jìn)行某種特定運(yùn)算,并把結(jié)果重新存入到操作棧頂箭阶。
分為兩類:對(duì)整形數(shù)據(jù)進(jìn)行運(yùn)算的指令和對(duì)浮點(diǎn)型數(shù)據(jù)進(jìn)行運(yùn)算的指令罩扇。
- 加法指令: 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
上面例子1中的執(zhí)行偏移地址13指令 iadd 就是將操作棧頂中兩個(gè)數(shù) 200 加上 100, 再把它的和 300 壓回棧滩届。
在除非指令(idiv, ldiv)以及求余指令(irem 和 lrem )中命浴, 當(dāng)出現(xiàn)除數(shù)為零時(shí)會(huì)導(dǎo)致虛擬機(jī)拋出 ArithmeticException 異常
3.轉(zhuǎn)換指令
轉(zhuǎn)換指令可以將兩種不同的數(shù)值類型進(jìn)行相互轉(zhuǎn)換月幌,一般用于顯示類型轉(zhuǎn)換
包含: i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l 和 d2f
其中 i 是 int, b 是 byte, c 是 char, s 是 short, l 是long, f 是 float, d 是 double 類型
例子2:
java 源碼
public class Test {
public static void main(String[] args) {
long zz = 10000;
int yy = (int)zz;
int a = 1;
int b = 2;
int c = a + b;
}
}
用 javap 生成的字節(jié)碼是
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=7, args_size=1
0: ldc2_w #2 // long 10000l , 將常量 1000l 加載到操作棧
3: lstore_1
4: lload_1
5: l2i // 這里對(duì)應(yīng) int yy = (int)zz; 進(jìn)行強(qiáng)轉(zhuǎn)
6: istore_3
7: iconst_1
8: istore 4
10: iconst_2
11: istore 5
13: iload 4
15: iload 5
17: iadd
18: istore 6
20: return
}
4.對(duì)象創(chuàng)建與訪問指令
在 Java 中一切皆為對(duì)象,類實(shí)例和數(shù)組都是對(duì)象鳄虱,但是 Java 虛擬機(jī)對(duì)類實(shí)例和數(shù)組的創(chuàng)建與操作使用了不同的字節(jié)碼指令倍踪。
- 創(chuàng)建類實(shí)例的指令: new
- 創(chuàng)建數(shù)組的指令: newarray, anewarry, multianewarray
- 訪問類字段(static 字段)和實(shí)例字段(非 static 字段)的指令: getfield, putfield, getstatic, putstatic
- 把一個(gè)數(shù)組元素加載到操作棧的指令: baload, caload, saload, iaload, laload, faload, daload, aaload
- 把一個(gè)操作數(shù)棧的值存儲(chǔ)到數(shù)組元素中的指令: bastore, castore, sastore, iastore, fastore, dastore, aastore
- 取數(shù)組長(zhǎng)度的指令 : arraylength
- 檢查類實(shí)例類型的指令: instanceof, checkcast
例3:
public class Test {
public static void main(String[] args) {
ByteTest byteTest = new ByteTest();
byteTest.name = "zhangsan";
byteTest.age = 13;
String[] list = new String[1];
list[0] = "one";
int size = list.length;
}
public static class ByteTest{
public static String name;
public int age;
}
}
生成的部分字節(jié)碼
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: new #2 // new 指令嫉到,new 一個(gè) ByteTest 對(duì)象
3: dup
4: invokespecial #3 // Method com/yxhuang/temp/Test$ByteTest."<init>":()V
7: astore_1
8: aload_1
9: pop
10: ldc #4 // String zhangsan
12: putstatic #5 // 訪問 ByteTest 靜態(tài)字段 name
15: aload_1
16: bipush 13
18: putfield #6 // 訪問 ByteTest 實(shí)例的字段 age
21: iconst_1
22: anewarray #7 // 創(chuàng)建數(shù)組
25: astore_2
26: aload_2
27: iconst_0
28: ldc #8 // String one
30: aastore
31: aload_2
32: arraylength // 取數(shù)組的長(zhǎng)度
33: istore_3
34: return
5.操作數(shù)棧管理指令
包括
- 將操作數(shù)棧的棧頂一個(gè)或兩個(gè)元素出棧: pop, pop2
- 復(fù)制棧頂一個(gè)或兩個(gè)數(shù)值并將復(fù)制值或雙份的復(fù)制值重新壓入棧頂:dup, dup2, dup_x1, dup_x2, dup2_x1, dup2_x2;
- 將棧最頂端的兩個(gè)數(shù)值互換: swap
其中 pop 是將棧頂一個(gè)元素出棧,pop2 是將棧頂?shù)膬蓚€(gè)元素出棧陌僵;
dup 是復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂, dup2 是復(fù)制棧頂一個(gè)(對(duì)于 long 或 double 類型)或 兩個(gè)(非 long, double 類型)的數(shù)值彈出;
dup_x1 是復(fù)制棧頂?shù)闹挡蓚€(gè)復(fù)制值壓入棧頂正蛙,dup_x2 復(fù)制棧頂一個(gè)(對(duì)于 long 或 double 類型)或 兩個(gè)(非 long, double 類型)的數(shù)值并壓入棧頂蒂阱;
dup2_x1 是 dup_x1 的雙倍版本响委, dup2_x2 是 dup_x2 的雙倍版本夹囚;
注:圖片來自于JVM 內(nèi)部原理(六)— Java 字節(jié)碼基礎(chǔ)之一
在 java 中 long 和 double 類型需要占用兩個(gè)存儲(chǔ)空間扇救,為了交換兩個(gè) double 值徽千,需要 dup2_x2
6.控制轉(zhuǎn)移指令
控制轉(zhuǎn)移指令可以讓 Java 虛擬機(jī)有條件或無條件地從指定的位置指令而不是控制轉(zhuǎn)移指令的下一條指令繼續(xù)執(zhí)行程序
- 條件分支
指令 | 例子 | 說明 |
---|---|---|
ifeq | if (i == 0) if (bool == false) |
當(dāng)棧頂 int 型數(shù)值等于0 或者是 false 跳轉(zhuǎn) |
ifne | if (i != 0) if (bool == true) |
當(dāng)棧頂 int 型數(shù)值不等于0 或者是 true 跳轉(zhuǎn) |
ifge | if (i >= 0) | 當(dāng)棧頂 int 型數(shù)值大于或等于0時(shí)跳轉(zhuǎn) |
ifgt | if (i > 0) | 當(dāng)棧頂 int 型數(shù)值大于0時(shí)跳轉(zhuǎn) |
ifle | if (i <= 0) | 當(dāng)棧頂 int 型數(shù)值小于或等于0時(shí)跳轉(zhuǎn) |
iflt | if (i < 0) | 當(dāng)棧頂 int 型數(shù)值小于0時(shí)跳轉(zhuǎn) |
if_icmple | 比較棧頂兩個(gè) int 型數(shù)值的大小,當(dāng)結(jié)果小于或等于0時(shí)跳轉(zhuǎn) |
-
復(fù)合條件分支
- tableswitch, 用于 switch 條件跳轉(zhuǎn)锨并, case 值連續(xù)(可變長(zhǎng)度指令)
- lookupswitch, 用于 switch 條件跳轉(zhuǎn)露该, case 值不連續(xù)(可變長(zhǎng)度指令)
-
無條件分支
- goto, 無條件跳轉(zhuǎn)
在 Java 虛擬機(jī)中,各種類型的比較最終都會(huì)轉(zhuǎn)化為 int 類型的比較第煮。
例4:
static int gt(int a, int b){
if (a > b){
return 1;
} else {
return -1;
}
}
// 生成的字節(jié)碼
static int gt(int, int);
descriptor: (II)I
flags: ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0 // 將 a 入棧 local[0]{a}
1: iload_1 // 將 b 入棧 local[1]解幼
2: if_icmple 7 // 如果 local[0] <= local[1] 跳去 7 抑党,即 iconst_m1
5: iconst_1 // 將 int 型 1 推送至棧頂
6: ireturn // 返回
7: iconst_m1 // 將 int 型 -1 推送至棧頂
8: ireturn // 返回
注意上面的 if_icmple 指令是比較棧頂兩個(gè) int 型數(shù)值的大小,當(dāng)結(jié)果小于或等于0時(shí)跳轉(zhuǎn)撵摆, 與我們代碼中 a > b 剛好是相反的
例5:
static int calc(int count){
int result = 0;
while (count > 0){
result += count--;
}
return result;
}
// 字節(jié)碼
static int calc(int);
descriptor: (I)I
flags: ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_0 // count 入棧
1: istore_1
2: iload_0
3: ifle 16 // 當(dāng)棧頂數(shù)值小于或等于0時(shí)調(diào)到 16底靠, 即跳出 while 選好
6: iload_1
7: iload_0
8: iinc 0, -1 // 自增或自減, 即 count--
11: iadd // result+
12: istore_1
13: goto 2 // 回到 2
16: iload_1
17: ireturn
例6:
static int calcSwitch(int type){
int result = 0;
switch (type){
case 1:
result = 10;
break;
case 2:
result = 11;
break;
}
return result;
}
// 字節(jié)碼
static int calcSwitch(int);
descriptor: (I)I
flags: ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_0
3: lookupswitch { // 2
1: 28
2: 34
default: 37
}
28: bipush 10
30: istore_1
31: goto 37
34: bipush 11
36: istore_1
37: iload_1
38: ireturn
7.方法調(diào)用和返回指令
- invokevirtual 指令用于調(diào)用對(duì)象的實(shí)例方法特铝,根據(jù)對(duì)象的實(shí)際類型進(jìn)行分派暑中;
- invokeinterface 指令用于調(diào)用接口方法,會(huì)在運(yùn)行時(shí)搜索一個(gè)實(shí)現(xiàn)了這個(gè)接口方法的對(duì)象鲫剿,找出合適的方法進(jìn)行調(diào)用鳄逾;
- invokespecial 指令用于調(diào)用一些需要特效處理的實(shí)例方法,包括實(shí)例初始化方法牵素、私有方法和父類方法
- invokestatic 指令調(diào)用類方法(static 方法)
- invokedynamic 指令用于在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法严衬,并執(zhí)行該方法
8.異常處理指令
arthrow 指令是 Java 程序中顯式拋出異常的操作(throw 語句)
9.同步指令
同步一段指令集序列通常是由 Java 語言中的 synchroined 語句來表示, Java 虛擬機(jī)的指令集中有 monitorenter 和 monitorexit 兩條指令來支持 synchronizd.
例7:
static void sycn(Object object){
synchronized (object){
}
}
// 相應(yīng)的字節(jié)碼
static void sycn(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
每條 monitorenter 指令都必須有其對(duì)應(yīng)的 monitorexit 指令笆呆,上面例子中的 3 和 5 是對(duì)應(yīng)的请琳。9 是處理異常的,這是編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理赠幕,同步指令也需要在異常的時(shí)候退出俄精,11 monitorexit 就是編譯器自動(dòng)生成在異常時(shí)退出的指令。
四榕堰、參考
周志明《深入理解 Java 虛擬機(jī)》