什么是字節碼?
java bytecode 由單字節(byte)的指令組成,理論上最多支持 256 個操作碼(opcode),實際上 Java 只使用了 200 個左右的操作碼,還有一些操作碼則保留給調試操作。
根據指令的性質,主要分為四大類:
- 棧操作指令,包括與局部變量交互的指令。
- 程序流程控制指令。
- 對象操作指令,包括方法調用指令。
- 算術運算以及類型轉換指令。
一、如何生成字節碼?
其實字節碼就是 class 文件,如何生成 class 文件呢?就是使用 javac 命令。
# 生成字節碼
javac -g:vars HelloByteCode.java
# 查看字節碼
javap -verbose HelloByteCode
二、字節碼指令
對於大部分為與數據類型相關的字節碼指令,他們的操作碼助記符中都有特殊的字符來表明專門為哪種數據類型服務:i代表對int類型的數據操作、l代表long、s代表short、b代表byte、c代表char、f代表float、d代表double、a代表reference。
1、加載和存儲指令
- 加載局部變量到操作棧:
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n> - 加載常量到操作棧:
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d> - 將操作數棧存儲到局部變量表:
istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
2、運算指令(運算結果會自動入棧)
- 加法指令:
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
3、類型轉換
JVM 對類型寬化自然支持,並不需要執行指令,但是對類型窄化需要執行指令:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
4、對象的創建及訪問
- 創建類實例:
new - 訪問類字段或實例字段:
getfield、putfield、getstatic、putstatic
5、數組
- 創建數組:
newarray、newwarray、multianewarray - 加載數組到操作數棧:
baload、caload、saload、iaload、laload、faload、daload、aaload - 將操作數棧存儲到數組元素:
bastore、castore、sastore、iastore、fastore、dastore、aastore - 取數組長度的指令:
arraylength
6、流程控制
- 條件判斷:
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
7、方法調用和返回指令(調用之后數據依然在操作數棧中)
invokevirtual:用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。invokeinterface:用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。invokespecial:用於調用一些需要特殊處理的實例方法,包括實例初始化方法(§2.9)、私有方法和父類方法。invokestatic:指令用於調用類方法(static方法)。
8、返回值指令
ireturn(當返回值是boolean、byte、char、short和int類型時使用)lreturnfreturndreturnareturn- 另外還有一條
return指令供聲明為void的方法、實例初始化方法、類和接口的類初始化方法使用
三、閱讀字節碼文件
閱讀字節碼文件之前,先了解一下 JVM 有哪些常用的操作碼(opcode),官方文檔。
例子:
public class HelloByteCode {
public static void main(String[] args) {
System.out.println("Hello ByteCode.");
// 基本類型定義
byte b1 = 100;
short s1 = 30000;
int i1 = 50;
int i2 = 200;
int i3 = 40000;
long l1 = 300;
long l2 = 1000000;
float f1 = 10.5F;
double d1 = 20.5D;
// 引用類型定義
String str1 = "Hello";
// 四則運算
int sum = i1 + i2;
int sub = i1 - i2;
int mul = i1 * i2;
int dvi = i2 / i1;
if (sum == 0) {
System.out.println("sum == 0");
}
for (int i = 0; i < 3; i++) {
System.out.println("index:" + i);
}
}
}
編譯、查看、翻譯字節碼:
➜ java javac -g:vars com/snailwu/course/code/HelloByteCode.java
➜ java javap -verbose com/snailwu/course/code/HelloByteCode
// class 文件的位置
Classfile /Users/wu/GitLab/java-course-code/src/main/java/com/snailwu/course/code/HelloByteCode.class
// 最后修改時間,文件大小
Last modified 2021-6-28; size 1243 bytes
// MD5
MD5 checksum 944f6523f8b0126fb7d76753c9193800
// 全類名
public class com.snailwu.course.code.HelloByteCode
// 最低最高版本號
minor version: 0
major version: 52
// 類的修飾符
flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
#1 = Methodref #22.#58 // java/lang/Object."<init>":()V
#2 = Fieldref #59.#60 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #61 // Hello ByteCode.
#4 = Methodref #62.#63 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Integer 40000
#6 = Long 300l
#8 = Long 1000000l
#10 = Float 10.5f
#11 = Double 20.5d
#13 = String #64 // Hello
#14 = String #65 // sum == 0
#15 = Class #66 // java/lang/StringBuilder
#16 = Methodref #15.#58 // java/lang/StringBuilder."<init>":()V
#17 = String #67 // index:
#18 = Methodref #15.#68 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#19 = Methodref #15.#69 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#20 = Methodref #15.#70 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#21 = Class #71 // com/snailwu/course/code/HelloByteCode
#22 = Class #72 // java/lang/Object
#23 = Utf8 <init>
#24 = Utf8 ()V
#25 = Utf8 Code
#26 = Utf8 LocalVariableTable
#27 = Utf8 this
#28 = Utf8 Lcom/snailwu/course/code/HelloByteCode;
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Utf8 i
#32 = Utf8 I
#33 = Utf8 args
#34 = Utf8 [Ljava/lang/String;
#35 = Utf8 b1
#36 = Utf8 B
#37 = Utf8 s1
#38 = Utf8 S
#39 = Utf8 i1
#40 = Utf8 i2
#41 = Utf8 i3
#42 = Utf8 l1
#43 = Utf8 J
#44 = Utf8 l2
#45 = Utf8 f1
#46 = Utf8 F
#47 = Utf8 d1
#48 = Utf8 D
#49 = Utf8 str1
#50 = Utf8 Ljava/lang/String;
#51 = Utf8 sum
#52 = Utf8 sub
#53 = Utf8 mul
#54 = Utf8 dvi
#55 = Utf8 StackMapTable
#56 = Class #34 // "[Ljava/lang/String;"
#57 = Class #73 // java/lang/String
#58 = NameAndType #23:#24 // "<init>":()V
#59 = Class #74 // java/lang/System
#60 = NameAndType #75:#76 // out:Ljava/io/PrintStream;
#61 = Utf8 Hello ByteCode.
#62 = Class #77 // java/io/PrintStream
#63 = NameAndType #78:#79 // println:(Ljava/lang/String;)V
#64 = Utf8 Hello
#65 = Utf8 sum == 0
#66 = Utf8 java/lang/StringBuilder
#67 = Utf8 index:
#68 = NameAndType #80:#81 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#69 = NameAndType #80:#82 // append:(I)Ljava/lang/StringBuilder;
#70 = NameAndType #83:#84 // toString:()Ljava/lang/String;
#71 = Utf8 com/snailwu/course/code/HelloByteCode
#72 = Utf8 java/lang/Object
#73 = Utf8 java/lang/String
#74 = Utf8 java/lang/System
#75 = Utf8 out
#76 = Utf8 Ljava/io/PrintStream;
#77 = Utf8 java/io/PrintStream
#78 = Utf8 println
#79 = Utf8 (Ljava/lang/String;)V
#80 = Utf8 append
#81 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#82 = Utf8 (I)Ljava/lang/StringBuilder;
#83 = Utf8 toString
#84 = Utf8 ()Ljava/lang/String;
{
// 空的構造方法
public com.snailwu.course.code.HelloByteCode();
// 方法的修飾符:包括參數及返回值。
descriptor: ()V
flags: ACC_PUBLIC
Code:
// 為什么args_size為1?非靜態方法默認第一個參數為 this 對象。參看 LocalVariableTable
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/snailwu/course/code/HelloByteCode;
// main 方法
public static void main(java.lang.String[]);
// 方法的修飾符:包括參數及返回值。
descriptor: ([Ljava/lang/String;)V
// 修飾符
flags: ACC_PUBLIC, ACC_STATIC
// 代碼區
Code:
// 第一個參數是 args
stack=3, locals=19, args_size=1
// 獲取 PrintStream 實例
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 將常量加載到操作數棧
3: ldc #3 // String Hello ByteCode.
// 調用方法進行輸出
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 將 100 壓入操作數棧
8: bipush 100
// 將操作數棧棧頂元素存到本地變量slot為1的位置
10: istore_1
// 將 30000 壓棧
11: sipush 30000
// 將棧頂元素存到slot為2的位置
14: istore_2
// 將 50 壓棧
15: bipush 50
// 將棧頂元素存到slot為3的位置
17: istore_3
// 將 200 壓棧
18: sipush 200
// 將棧頂元素存到slot為4的位置
21: istore 4
// 將 40000 壓棧
23: ldc #5 // int 40000
// 將棧頂元素存到slot為5的位置
25: istore 5
// 將 300 壓棧,long占用兩個slot
27: ldc2_w #6 // long 300l
// 將棧頂元素存到slot為6、7的位置
30: lstore 6
// 將 1000000 壓棧,long占用兩個slot
32: ldc2_w #8 // long 1000000l
// 將棧頂元素存到slot為8、9的位置
35: lstore 8
// 將 10.5 壓棧
37: ldc #10 // float 10.5f
// 將棧頂元素存到slot為10的位置
39: fstore 10
// 將 20.5 壓棧
41: ldc2_w #11 // double 20.5d
// 將棧頂元素存到slot為11、12的位置
44: dstore 11
// 將 Hello 壓棧
46: ldc #13 // String Hello
/ 將棧頂元素存到slot為13的位置
48: astore 13
// 加載slot為3處的值到棧頂
50: iload_3
// 加載slot為4處的值到棧頂
51: iload 4
// 將棧頂兩個元素彈出,進行相加,然后結果存入棧頂
53: iadd
// 將棧頂元素存入slot為14的位置
54: istore 14
// 加載slot為3處的值到棧頂
56: iload_3
// 加載slot為4處的值到棧頂
57: iload 4
// 將棧頂兩個元素彈出,進行相減,然后結果存入棧頂
59: isub
// 將棧頂元素存入slot為15的位置
60: istore 15
// 加載slot為3處的值到棧頂
62: iload_3
// 加載slot為4處的值到棧頂
63: iload 4
// 將棧頂兩個元素彈出,進行相乘,然后結果存入棧頂
65: imul
// 將棧頂元素存入slot為16的位置
66: istore 16
// 加載slot為4處的值到棧頂
68: iload 4
// 加載slot為3處的值到棧頂
70: iload_3
// 將棧頂兩個元素彈出,進行相除,然后結果存入棧頂
71: idiv
// 將棧頂元素存入slot為17的位置
72: istore 17
// 加載slot為14處的值到棧頂
74: iload 14
// 如果棧頂元素不等於0,跳轉到偏移量為87的位置往下執行
76: ifne 87
// 如果棧頂元素等於0,執行76到87之間的代碼
// 79到84:輸出 "sum == 0"
79: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
82: ldc #14 // String sum == 0
84: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 將 0 壓棧
87: iconst_0
// 將棧頂元素存入slot為18的位置
88: istore 18
// 加載slot為18處的值到棧頂
90: iload 18
// 將 3 壓入棧頂
92: iconst_3
// 彈出棧頂兩個元素進行比較,如果 val1(第一個彈出的元素:3) >= val2(第二個彈出的元素:0) 則跳轉到偏移量為128的位置往下執行
93: if_icmpge 128
// 獲取 PrintStream 實例
96: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// new 關鍵字,新建對象 StringBuilder,並將引用壓入棧頂
99: new #15 // class java/lang/StringBuilder
// 復制棧頂的元素並壓入棧頂,就是把 StringBuilder 的引用復制一份,invokespecial 會消耗一個 引用,如果不復制,后面的 append 就沒有引用可以使用了
102: dup
// 執行 StringBuilder 的初始化方法,消耗棧頂的一個 StringBuilder 對象引用,返回值是 void,所以這個引用消耗了就沒了
103: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V
// 將 "index" 壓入棧頂
106: ldc #17 // String index:
// 彈出棧頂的 StringBuilder 引用執行 append 方法,執行結果(返回值:StringBuilder類型的)壓入棧頂
108: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
// 將slot為18處的值加載到棧頂
111: iload 18
// 彈出棧頂的 StringBuilder 引用執行 append 方法,執行結果(返回值:StringBuilder類型的)壓入棧頂
113: invokevirtual #19 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
// 彈出棧頂的 StringBuilder 引用執行 String 方法,執行結果(返回值:String類型的)壓入棧頂
116: invokevirtual #20 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
// 彈出棧頂元素,執行 println 方法
119: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// slot為18出的值自增1
122: iinc 18, 1
// 跳轉到偏移量為90的位置繼續執行
125: goto 90
// 執行返回
128: return
// 本地變量表
LocalVariableTable:
Start Length Slot Name Signature
90 38 18 i I
0 129 0 args [Ljava/lang/String;
11 118 1 b1 B
15 114 2 s1 S
18 111 3 i1 I
23 106 4 i2 I
27 102 5 i3 I
32 97 6 l1 J
37 92 8 l2 J
41 88 10 f1 F
46 83 11 d1 D
50 79 13 str1 Ljava/lang/String;
56 73 14 sum I
62 67 15 sub I
68 61 16 mul I
74 55 17 dvi I
// 棧表
StackMapTable: number_of_entries = 3
frame_type = 255 /* full_frame */
offset_delta = 87
locals = [ class "[Ljava/lang/String;", int, int, int, int, int, long, long, float, double, class java/lang/String, int, int, int, int ]
stack = []
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 250 /* chop */
offset_delta = 37
}
是不是字節碼文件也很容易閱讀。
四、指令總結
每一個線程都有一個保存幀的棧。在每一個方法調用的時候創建一個幀。一個幀包括了三個部分:操作棧,局部變量數組,和一個對當前方法所屬類的常量池的引用。
局部變量數組也被稱之為局部變量表,它包含了方法的參數,也用於保存一些局部變量的值。參數值得存放總是在局部變量數組的index0開始的。如果當前幀是由構造函數或者實例方法創建的,那么該對象引用將會存放在location0處,然后才開始存放其余的參數。
局部變量表的大小由編譯時決定,同時也依賴於局部變量的數量和一些方法的大小。操作棧是一個(LIFO)棧,用於壓入和取出值,其大小也在編譯時決定。某些opcode指令將值壓入操作棧,其余的opcode指令將操作數取出棧。使用它們后再把結果壓入棧。操作棧也用於接收從方法中返回的值。
