本文詳細介紹瞭如何使用javap查看java方法中的字節碼、以及各種字節碼的含義,並且配以完善的案例,一步步,從頭到尾帶領大家翻譯javap的輸出。在文末還附有JVM字節碼指令集表。
本文不適合沒有JVM基礎的初學者,看文章之前希望你有如下基本技能:瞭解JVM的一些基本概念,比如什麼是符號引用?什麼是字面量?瞭解class文件的基本結構,瞭解基於棧的JVM方法執行的棧基本結構!關於class文件的結構可以看:Java的 Class(類)文件結構詳解。
看完本文,你可能獲得如下知識:JVM字節碼的入門、如何查看和分析字節碼的執行、加深對JVM中的棧的理解,基於字節碼層面,或許還能爲你解開一些原始的疑惑,比如:爲什麼finally塊中的語句總會被執行?
1 字節碼概述
字節碼是一套設計用來在Java虛擬機中執行的高度優化的指令集。Java字節碼對於虛擬機,就好像彙編語言對於計算機,屬於基本執行指令,或者說是Java代碼的在JVM中的最小執行單元。JVM只關心字節碼,而不關心源文件是屬於在哪個平臺用哪種語言編寫的,字節碼是實現JVM平臺無關性和語言無關性的基石。
JVM字節碼指令由一個字節長度的、代表着某種特定操作含義的數字稱爲操作碼,Opcode),以及跟隨其後的零到多個代表此操作所需參數(稱爲操作數,Operands)構成。
所謂一個字節的長度,也就是8位二進制數字,也就是兩位十六進制數字。
每一個字節碼還擁有它對應的助記符形式。顧名思義,助記符就是幫助我們查看、理解字節碼的含義的符號,一般我們看字節碼操作都通過是看助記符來分辨的。但是實際的執行運行並不存在助記符這些東西,都是根據字節碼的值來執行。
由於操作碼的長度爲一個字節,因此操作碼(指令)最多不超過256條;
對於操作數長度超過了一個字節的情況(取決於操作碼):由於Class文件格式放棄了編譯後代碼的操作數長度對齊,所以,當JVM處理超過一個字節長度的數據時,需要在運行時從字節中重建出具體數據的結構,如16位二進制數據需要(byte1 << 8)|byte2操作(Big-Endian 順序存儲——即高位在前的字節序)。
2 基於javap的字節碼解析
2.1 javap介紹
如果直接打開class文件,只能查看數字形式的字節碼。
我們可以使用javap工具,反編譯class文件,將數字類型的字節碼轉換成我們能看懂的格式,進而快捷的查看一個java類的常量池、方法表、code屬性(方法字節碼)、局部變量表、異常表等等信息。在這裏,我認爲javap反編譯之後輸出的數據,並不是傳統意義上的彙編指令,只是The Java Virtual Machine Instruction Set(Java 虛擬機指令集)的一種方便人們理解的表現形式。
javap的格式爲: javap < options > < classes >
其中options選項如下:
-version | 版本信息,其實是當前javap所在jdk的版本信息,不是cass在哪個jdk下生成的。 |
-l | 輸出行號和本地變量表 |
-c | 對代碼進行反彙編,主要是針對方法的代碼 |
-v -verbose | 不僅會輸出行號、本地變量表信息、反編譯彙編代碼,還會輸出當前類用到的常量池等詳細信息。 |
-pubic | 僅顯示公共類和成員 |
-protected | 顯示受保護的/公共類和成員 |
-package | 顯示程序包/受保護的/公共類 和成員 (默認) |
-p -private | 顯示所有類和成員 |
-s | 輸出內部類型簽名 |
-sysinfo | 顯示正在處理的類的系統信息 (路徑, 大小, 日期, MD5 散列) |
-constants | 顯示靜態最終常量 |
-classpath |
指定查找用戶類文件的位置 |
-bootclasspath |
覆蓋引導類文件的位置 |
一般常用的是-v -l -c三個選項,-v展示的最全面。官方javap介紹地址爲:javap
2.2 javap案例
2.2.1 源碼類
源碼類使用在介紹class文件結構時候使用的源碼類:Java的 Class(類)文件結構詳解。方便連續學習。這裏再貼出來一下:
public class ClassFile {
public static final String J = "2222222";
private int k;
public int getK() {
return k;
}
public void setK(int k) throws Exception {
try {
this.k = k;
} catch (IllegalStateException e) {
e.printStackTrace();
} finally {
}
}
public static void main(String[] args) {
}
}
2.2.2 javap輸出解析
對源碼的class文件,使用javap -v ClassFile.class指令,得到如下信息(灰色的文字是後來自己加的註釋)。
對於初學者,應該一行行的仔細查看其含義,對於重要的部分單獨拿出來講解,比如方法字節碼部分。
Classfile /J:/Idea/jvm/target/classes/com/ikang/JVM/classfile/ClassFile.class //生成該class文件的源文件名稱和路徑
Last modified 2020-4-8; size 960 bytes //上一次修改時間和大小
MD5 checksum fcd8ef238722b6dab50c0495a9673a1f //MD5信息
Compiled from "ClassFile.java" //編譯自ClassFile.java類
public class com.ikang.JVM.classfile.ClassFile
minor version: 0 //小版本號
major version: 52 //大版本號
flags: ACC_PUBLIC, ACC_SUPER //類訪問標記, ACC_PUBLIC表示public, ACC_SUPER表示允許使用invokespecial字節碼指令的新語意
//class常量池開始,可以看出有46個常量
Constant pool:
//第1個常量是CONSTANT_Methodref_info類型,表示類中方法的符號引用,具有兩個索引屬性,分別指向第6個和第37個常量,其最終值爲java/lang/Object."<init>":()V
//這明顯是< init >方法 的符號引用,編譯時自動生成的。
#1 = Methodref #6.#37 // java/lang/Object."<init>":()V
//第2個常量是CONSTANT_Fieldref_info類型,表示類中字段的符號引用,具有兩個索引屬性,分別指向第5個和第38個常量 其最終值爲com/ikang/JVM/classfile/ClassFile.k:I
//這明顯是k字段的符號引用
#2 = Fieldref #5.#38 // com/ikang/JVM/classfile/ClassFile.k:I
//第3個常量是CONSTANT_Class_info類型,表示類或接口的符號引用,具有一個索引屬性,指向第39個常量 其最終值爲java/lang/IllegalStateException
//這明顯是引入的IllegalStateException異常類的符號引用
#3 = Class #39 // java/lang/IllegalStateException
//第4個常量是CONSTANT_Methodref_info類型,表示類中方法的符號引用,具有兩個索引屬性,分別指向第3個和第40個常量,其最終值爲java/lang/IllegalStateException.printStackTrace:()V
//這明顯是e.printStackTrace()方法的符號引用
#4 = Methodref #3.#40 // java/lang/IllegalStateException.printStackTrace:()V
//第5個常量是CONSTANT_Class_info類型,表示類或接口的符號引用,具有一個索引屬性,指向第41個常量 其最終值爲com/ikang/JVM/classfile/ClassFile
//這明顯是本類(ClassFile類)的符號引用
#5 = Class #41 // com/ikang/JVM/classfile/ClassFile
//第6個常量是CONSTANT_Class_info類型,表示類或接口的符號引用,具有一個索引屬性,指向第41個常量 其最終值爲java/lang/Object
//這明顯是本類的父類Object的符號引用
#6 = Class #42 // java/lang/Object
//第7個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是J
//這明顯儲存的常量J的名稱的字面值
#7 = Utf8 J
//第7個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是Ljava/lang/String;
//這明顯表示一個對象類型的字段描述符的字面值,String類型
#8 = Utf8 Ljava/lang/String;
//第9個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是ConstantValue
//這明顯表示常量字段的ConstantValue屬性的名字的字符串字面值
#9 = Utf8 ConstantValue
//第10個常量是CONSTANT_String_info類型,表示字符串類型字面量,用來表示類型,具有一個指向具體字面值的索引43, 這裏的具體值是2222222
//這明顯是常量 J的字面量,因爲是字符串類型
#10 = String #43 // 2222222
//第11個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 k
//這明顯儲存的變量J的名稱的字面值
#11 = Utf8 k
//第12個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 I
//這明顯表示一個int類型的字段描述符的字面值
#12 = Utf8 I
//第13個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 <init>
//這明顯表示<init>方法名稱的字面值
#13 = Utf8 <init>
//第14個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 ()V
//這明顯表示一個返回值爲void類型、沒有參數的方法描述符的字面值,那麼可以表示構造方法的方法描述符的字面值
#14 = Utf8 ()V
//第15個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 Code
//這明顯表示方法中的Code屬性的名稱的字面值
#15 = Utf8 Code
//第16個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 k
//這明顯表示方法的Code屬性中的LineNumberTable屬性的名稱的字面值
#16 = Utf8 LineNumberTable
//第17個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 k
//這明顯表示方法的Code屬性中的LocalVariableTable屬性的名稱的字面值
#17 = Utf8 LocalVariableTable
//第18個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 this
//這明顯表示方法的局部變量this的名稱的字面值
#18 = Utf8 this
//第19個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 Lcom/ikang/JVM/classfile/ClassFile;
//這明顯表示一個對象類型的字段描述符的字面值,ClassFile類型
#19 = Utf8 Lcom/ikang/JVM/classfile/ClassFile;
//第20個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 getK
//這明顯表示getK方法名稱的字面值
#20 = Utf8 getK
//第21個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 ()I
//這明顯表示一個返回值爲int類型、沒有參數的方法描述符的字面值,那麼可以表示getK方法的方法描述符的字面值
#21 = Utf8 ()I
//第22個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 setK
//這明顯表示setK方法名稱的字面值
#22 = Utf8 setK
//第23個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 (I)V
//這明顯表示一個返回值爲void類型、參數爲int類型的方法描述符的字面值,那麼可以表示setK方法的方法描述符的字面值
#23 = Utf8 (I)V
//第24個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 e
//這個e是什麼呢?實際上是表示參數的名稱的字面值
#24 = Utf8 e
//第25個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是Ljava/lang/IllegalStateException;
//這明顯表示一個對象類型的字段描述符的字面值,IllegalStateException類型
#25 = Utf8 Ljava/lang/IllegalStateException;
//第26個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是Ljava/lang/IllegalStateException;
//這明顯表示方法的Code屬性中的StackMapTable屬性的名稱的字面值
#26 = Utf8 StackMapTable
//第27個常量是CONSTANT_Class_info類型,表示類或接口的符號引用,具有一個索引屬性,指向第39個常量 其最終值爲java/lang/IllegalStateException
//這明顯是IllegalStateException類的符號引用
#27 = Class #39 // java/lang/IllegalStateException
//第28個常量是CONSTANT_Class_info類型,表示類或接口的符號引用,具有一個索引屬性,指向第44個常量 其最終值爲java/lang/Throwable
//這明顯是Exception的父類Throwable類的符號引用
#28 = Class #44 // java/lang/Throwable
//第29個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是Exceptions
//加了個s,肯定不是類名字面量了,實際上它是方法表中的Exceptions異常表屬性的名字 字符串字面量
#29 = Utf8 Exceptions
//第30個常量是CONSTANT_Class_info類型,表示類或接口的符號引用,具有一個索引屬性,指向第45個常量 其最終值爲java/lang/Exception
//這明顯是Exception類的符號引用
#30 = Class #45 // java/lang/Exception
//第31個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是main
//這明顯是main方法的方法名字 字符串字面量
#31 = Utf8 main
//第32個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 ([Ljava/lang/String;)V
//這明顯表示一個返回值爲void類型、參數爲String一維數組類型的方法描述符的字面值,那麼可以表示main方法的方法描述符的字面值
#32 = Utf8 ([Ljava/lang/String;)V
//第33個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 ([Ljava/lang/String;)V
//這明顯表示main方法的參數名的字符串字面量
#33 = Utf8 args
//第34個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 [Ljava/lang/String;
//這明顯表示一個對象類型的字段描述符的字面值,String[]類型
#34 = Utf8 [Ljava/lang/String;
//第35個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 SourceFile
//這明顯表示SourceFile屬性的名字的字面值
#35 = Utf8 SourceFile
//第36個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 ClassFile.java
//這明顯表示SourceFile屬性的具體值的字面值
#36 = Utf8 ClassFile.java
//第37個常量是CONSTANT_NameAndType_info類型,表示字段或方法的部分符號引用,還持有兩個索引,分別指向13和14個字符串,這裏的最終值是 "<init>":()V,我們回過頭去看第13和14個字符串,它們儲存的是具體的字面值。
//這個常量被我們的第1個常量持有,即表示< init >方法的部分符號引用
#37 = NameAndType #13:#14 // "<init>":()V
//第38個常量是CONSTANT_NameAndType_info類型,表示字段或方法的部分符號引用,還持有兩個索引,分別指向11和12個字符串,這裏的最終值是 k:I,我們回過頭去看第11和12個字符串,它們儲存的是具體的字面值。
//這個常量被我們的第2個常量持有,即表示k字段的部分符號引用
#38 = NameAndType #11:#12 // k:I
//第39個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 java/lang/IllegalStateException
//這個常量被我們的第3、27個常量持有,這明顯表示儲存IllegalStateException類的符號引用的具體字面值
#39 = Utf8 java/lang/IllegalStateException
//第40個常量是CONSTANT_NameAndType_info類型,表示字段或方法的部分符號引用,還持有兩個索引,分別指向46和14個字符串,這裏的最終值是 k:I,我們去看第46和14個字符串,它們儲存的是具體的字面值。
//這個常量被我們的第4個常量持有,即表示printStackTrace方法的部分符號引用
#40 = NameAndType #46:#14 // printStackTrace:()V
//第41個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 com/ikang/JVM/classfile/ClassFile
//這個常量被我們的第5個常量持有,這明顯表示儲存本類(ClassFile類)的符號引用的具體字面值
#41 = Utf8 com/ikang/JVM/classfile/ClassFile
//第42個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 com/ikang/JVM/classfile/ClassFile
//這個常量被我們的第6個常量持有,這明顯表示儲存本Object類的符號引用的具體字面值
#42 = Utf8 java/lang/Object
//第43個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 2222222
//這個常量被我們的第10個常量持有,這明顯表示儲存常量J的具體值
#43 = Utf8 2222222
//第44個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 java/lang/Throwable
//這個常量被我們的第28個常量持有,這明顯表示儲存Exception的父類Throwable類的符號引用具體值
#44 = Utf8 java/lang/Throwable
//第45個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 java/lang/Throwable
//這個常量被我們的第30個常量持有,這明顯表示儲存Exception類的符號引用具體值
#45 = Utf8 java/lang/Exception
//第46個常量是CONSTANT_utf8_info類型,表示UTF-8編碼的字符串,用來存儲具體的字符串字面值 這裏的值是 printStackTrace
//這個常量被我們的第40個常量持有,這明顯表示printStackTrace方法名的具體值
#46 = Utf8 printStackTrace
//class常量池結束,後面是字段表和方法表
{
//常量字段J
public static final java.lang.String J;
//常量字段J的字段描述符 String類型
descriptor: Ljava/lang/String;
//常量字段J的訪問標記 public static final
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
//常量字段J的額外ConstantValue屬性,該屬性中又包括常量字段的類型和具體值,這裏可以看出來,對於常量字段在編譯時就確定了值,對於常量字段,在類加載的準備階段就已經通過ConstantValue初始化爲最終值了,對常量字段的引用不會導致類進入“初始化”階段。
ConstantValue: String 2222222
/*方法表的默認構造方法開始*/
public com.ikang.JVM.classfile.ClassFile();
//方法描述符
descriptor: ()V
//方法訪問標記
flags: ACC_PUBLIC
//方法Code屬性
Code:
//棧最大深度1,局部變量最大數量1,參數1
stack=1, locals=1, args_size=1
/*下面是方法字節碼,對於方法字節碼,在後面提出來重點說明*/
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//行號表, 用於描述Java源代碼行號與字節碼行號(字節碼偏移量)之間的對應關係,每一行第一個數字對應代碼行數,第二個數字對應前面code中字節碼字節碼指令左邊的數字。一行可以對應多個字節碼指令。
LineNumberTable:
line 4: 0
//局部變量表, start+length表示這個變量在字節碼中的生命週期起始和結束的偏移位置,Name就是變量的名字,slot就是這個變量在局部變量表中的槽位(槽位可複用,從0開始),name就是變量名稱,Signatur局部變量的字段類型描述符;
//下面的含義就是名叫this的局部變量的字節碼生命週期從頭0到結尾5(不包括),佔用第一個槽位(索引0),變量是ClassFile類型。
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
/*方法表的默認構造方法 結束*/
/*方法表的getk方法 開始*/
public int getK();
//方法描述符,很簡單,表示返回值爲int ,參數爲空
descriptor: ()I
//訪問標記 public
flags: ACC_PUBLIC
//Code屬性,包括方法字節碼,行號表,局部變量表
Code:
//棧最大深度1,局部變量最大數量1,參數1
stack=1, locals=1, args_size=1
/*下面是方法字節碼,對於方法字節碼,在後面提出來重點說明*/
0: aload_0
1: getfield #2 // Field k:I
4: ireturn
//行號表
LineNumberTable:
line 10: 0
//局部變量表,只有一個this變量
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
/*方法表的getk方法 結束*/
/*方法表的setK方法 開始*/
public void setK(int) throws java.lang.Exception;
//方法描述符,很簡單,表示返回值爲void ,參數爲int
descriptor: (I)V
//訪問標記 public
flags: ACC_PUBLIC
//Code屬性,包括方法字節碼,行號表,局部變量表,該方法還包括異常表、棧圖
Code:
//棧最大深度2,局部變量最大數量4,參數2
stack=2, locals=4, args_size=2
/*下面是方法字節碼,對於方法字節碼,在後面提出來重點說明*/
0: aload_0
1: iload_1
2: putfield #2 // Field k:I
5: goto 19
8: astore_2
9: aload_2
10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V
13: goto 19
16: astore_3
17: aload_3
18: athrow
19: return
//異常表屬性, 在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的,而是採用異常表來完成的。異常表是由try-catch語句生成的。具體結構在class文件結構那篇文章處有介紹。
Exception table:
from to target type
//第一行表示 0~5(不包括) 行出現的 IllegalStateException 異常,直接跳轉到 8(astore_2) 行;java代碼層面就是:如果try語句塊中出現屬於IllegalStateException或其子類的異常,則轉到catch語句塊處理。
0 5 8 Class java/lang/IllegalStateException
//第二行表示 0~5(不包括) 行出現的 其餘的所有異常, 直接跳轉到 16(astore_3) 行,java代碼層面就是:如果try語句塊中出現不屬於IllegalStateException或其子類的異常,則轉到finally語句塊處理。從這裏可以看出,finally關鍵字的語義也是由異常表實現的。
0 5 16 any
//第三行表示 8~13(不包括) 行出現的 其餘的所有異常, 直接跳轉到 16(astore_3) 行;java代碼層面就是:如果catch語句塊中出現任何異常,則轉到finally語句塊處理。
8 13 16 any
//行號表
LineNumberTable:
line 16: 0
line 20: 5
line 17: 8
line 18: 9
line 20: 13
line 19: 16
line 21: 19
//局部變量表,有3個局部變量
LocalVariableTable:
Start Length Slot Name Signature
9 4 2 e Ljava/lang/IllegalStateException;
0 20 0 this Lcom/ikang/JVM/classfile/ClassFile;
0 20 1 k I
//棧圖,該屬性不包含運行時所需的信息,僅僅用於用於加快Class文件的類型檢驗。
StackMapTable: number_of_entries = 3
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/IllegalStateException ]
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 2 /* same */
//列舉異常,表示一個方法可能拋出的異常,通常是由方法的throws 關鍵字指定的,這和Code屬性中的異常表不一樣。
Exceptions:
throws java.lang.Exception
/*方法表的setK方法 結束*/
/*方法表的main方法 開始*/
public static void main(java.lang.String[]);
//方法描述符,很簡單,表示返回值爲void ,參數爲String[]
descriptor: ([Ljava/lang/String;)V
//訪問標記,public static
flags: ACC_PUBLIC, ACC_STATIC
//code屬性
Code:
stack=0, locals=1, args_size=1
//由於沒有代碼,因此只有return一個字節碼,表示方法的結束.
0: return
LineNumberTable:
line 24: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
}
//記錄class文件的源文件 ClassFile.java
SourceFile: "ClassFile.java"
2.2.2.1 構造方法的字節碼解析
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
左邊的數字表示字節碼偏移量(從0開始),字節碼的偏移量和前面的字節碼長度有關係,例如aload_0是第一個字節碼,自然偏移量是0,它本身佔一個字節,因此 invokespecial就從索引1開始,invokespecial本身佔據一個字節,但是接收一個兩個字節的無符號數參數,因此整個invokespecial指令佔據3個字節,故後續的return指令位於偏移量4處。
在最開始時,局部變量表中具有一個變量this。局部變量表第0項爲this引用,表示當前對象。在Java 中, 對千所有的非靜態函數調用, 爲了能順利訪問this對象,都會將對象的引用放置在局部變量表第0 個槽位,如下圖:
執行第一條指令,aload_0,表示把局部變量第 1 個引用型局部變量推到操作數棧,即把this對象引用壓入操作數棧的棧頂(操作數棧相當於虛擬機的工作區,主要用於字節碼的執行——大多數指令都要從這裏先壓入原始數據,然後彈出數據,執行運算,然後把結果壓回操作數棧,最後把結果賦給局部變量表的變量),如下圖:
執行第二條指令,invokespecial, 表示對棧頂對象的私有、構造、父類方法進行調用,這些方法的特點是調用目標在編譯時就確定了,對這些方法的調用又稱爲“解析”。該指令接收一個2個字節長度的無符號參數,用於構建一個當前類的運行時常量池的索引值,該索引所指向的運行時常量池項應當是一個方法的符號引用,表示將執行棧頂對象對應的該方法。
javap已經幫我們算出來了,該索引就是值爲1的索引處的常量項,我們回去看看第一項常量,是< init >方法的符號引用。即調用實例初始化方法(< init >)來完成對象的初始化,就是this指定的對象的調用< init >方法完成初始化。
第二條指令執行完,即該類對象被初始化完畢,執行第三條指令,return,表示方法結束,並從當前方法返回 void,並清空棧空間數據包括操作數棧和局部變量表。
從上面的三條指令可以看出來,在無參構造方法調用了< init >方法對對象進行初始化, < init >方法用於將對象字段的零值初始化爲指定值。
2.2.2.2 getK方法的字節碼解析
0: aload_0
1: getfield #2 // Field k:I
4: ireturn
在最開始時,同樣局部變量表中具有一個變量this,操作數棧爲空。
執行第一個指令,aload_0,表示把 局部變量第1個引用型局部變量推到操作數棧頂 ,即把this對象引用壓入操作數棧的棧頂。
執行第二個指令,getfield,表示 獲取指定對象的字段值,並將其值壓入棧頂 。該指令接收一個2個字節長度的無符號參數,用於構建一個當前類的運行時常量池的索引值,該索引所指向的運行時常量池項應當是一個字段的符號引用,指令執行後,該字段的值將被取出,並壓入到操作數棧頂。很明顯,這裏指向第二項常量,我們回去看看第二項常量,的確是k字段 的符號引用。第二個指令執行完畢後如下圖:
執行第三個指令,ireturn,表示結束方法並返回一個int 類型數據。很明顯是將操作數棧的棧頂值返回(對於執行引擎來說,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作)。
2.2.2.3 getKsetK方法的字節碼
0: aload_0
1: iload_1
2: putfield #2 // Field k:I
5: goto 19
8: astore_2
9: aload_2
10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V
13: goto 19
16: astore_3
17: aload_3
18: athrow
19: return
//異常表屬性, 在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的,而是採用異常表來完成的。異常表是由try-catch語句生成的。具體結構在class文件結構處有介紹。
Exception table:
from to target type
//第一行表示 0~5(不包括) 行出現的 IllegalStateException 異常,直接跳轉到 8(astore_2) 行;java代碼層面就是:如果try語句塊中出現屬於IllegalStateException或其子類的異常,則轉到catch語句塊處理。
0 5 8 Class java/lang/IllegalStateException
//第二行表示 0~5(不包括) 行出現的 其餘的所有異常, 直接跳轉到 16(astore_3) 行,java代碼層面就是:如果try語句塊中出現不屬於IllegalStateException或其子類的異常,則轉到finally語句塊處理。從這裏可以看出,finally關鍵字的語義也是由異常表實現的。
0 5 16 any
//第三行表示 8~13(不包括)行出現的 其餘的所有異常, 直接跳轉到 16(astore_3) 行;java代碼層面就是:如果catch語句塊中出現任何異常,則轉到finally語句塊處理。
8 13 16 any
該方法的字節碼明顯變多了,主要加了一些異常處理邏輯。不過一個一個看,也很簡單。
執行第1個指令,aload_0,表示把局部變量第1個引用型局部變量推到操作數棧頂,即把this對象引用壓入操作數棧的棧頂。
執行第2個指令,iload_1,表示把局部變量第2個int型局部變量推到操作數棧頂,即把k字段的值壓入操作數棧的棧頂。
執行第3個指令,putfield,表示設置對象字段的值。該指令接收一個2個字節長度的無符號參數,用於構建一個當前類的運行時常量池的索引值,該索引所指向的運行時常量池項應當是一個字段的符號引用,指令執行後,操作數棧中的該字段所屬的對象this和具體的值,將被彈出棧。putfield的參數指向常量池第二個常量,明顯表示k字段。
執行第4個指令,goto,表示無條件分支跳轉。 該指令接收一個2個字節長度的有符號參數,用於構建一個16 位有符號的分支偏移量。指令執行後,程序將會轉到這個goto 指令之後的,由上述偏移量確定的目標地址上繼續執行。這個目標地址必須處於goto 指令所在的方法之中。
很明顯,後面的偏移量是19,這說明爲k賦值之後,就可以執行return了。因爲本代碼finally塊中沒有定義代碼,實際上如果finally中有代碼,那麼將會在每一個goto之前插入finally的字節碼,這也從字節碼的層面保證了finally語句塊必須執行!
執行第5個指令,astore_2,表示。把棧頂引用型數值存入第3個局部變量表位置。 |
執行第6個指令,aload_2,表示把局部變量第3個引用型局部變量推到操作數棧。 |
執行第7個指令,invokevirtual,表示調用實例方法,依據實例的類型進行方法分派。該指令接收一個2個字節長度的無符號參數,用於構建一個當前類的運行時常量池的索引值,該索引所指向的運行時常量池項應當是一個方法的符號引用,這裏指向第四個常量,我們返回查看常量池,發現是printStackTrace方法的符號引用。 |
實際上,上面表格部分的字節碼,就是catch塊中的內容。我們根據異常表,當發生IllegalStateException異常,將會跳轉到astore_2,執行catch中的內容。
查看局部變量表可知,實際上astore_2是將IllegalStateException異常引用存放了局部變量表的第三個位置。
第6、7個指令實際上就是在調用printStackTrace方法。
執行第8個指令,goto,表示無條件分支跳轉。這是第二個goto指令,在代碼邏輯層面:表示catch執行完畢,finally語句塊執行完畢(本finally中沒有代碼),方法結束。
第9個指令,astore_3, 表示把棧頂引用型數值存入第4個局部變量表位置。能夠執行到這個指令,根據異常表,說明catch中出現了異常或者出現不屬於IllegalStateException或其子類的異常,將該異常類型,放到第四個局部變量表位置。我們去查看局部變量表,並沒有第四個異常變量,這說明這一段字節碼的邏輯不一定會執行,在編譯時還不確定該異常的類型。
第10個指令,aload_3, 表示把局部變量第4個引用型局部變量推到操作數棧。即將上面的異常,放到操作數棧。但是在該指令之前還會執行finally中的字節碼。
第11個指令,athrow, 表示將棧頂的異常拋出。即將上面的未能使用catch捕獲的異常拋出,並結束方法。
第12個指令,return,作爲方法結束的標誌。
到此,setk方法結束。
補充:
如果在finally中添加一個輸出語句:System.out.println(11);那麼字節碼將會如下:
我們可以看到,finally中的字節碼被插入到每一個可能的分支的最後,代碼層面就表示:無論如何,finally中代碼都將被執行,這是jvm在編譯時爲我們做出的字節碼級別的保證。
3 jclasslib替代javap
實際上,現在通過jclasslib工具可以完全替代javap命令,jclasslib可用於用於打開class文件,而且是可視化的,效果更好一些。jclasslib還提供了修改jar包中的Class文件的API。
爲什麼先介紹比較麻煩的javap?因爲javap是Java官方自帶的,先了解原生的工具之後,再來使用jclasslib,我們將會更加的得心應手。
jclasslib的github地址:jclasslib。
3.1 idea安裝jclasslib插件
- 使用 ALT+CTRL+S 打開setting
- 選擇plugins(插件),搜索jclasslib,然後安裝,重啓
- 選擇要打開的class文件,點擊上面的view,點擊show bytecode with jclasslib,在右側就會出現可視化界面
- 右側的可視化界面,可以看出來還是很簡單的,這裏就不介紹了。
3.2 直接安裝jclasslib
當然有些開發者沒有使用idea, github上這裏也可以直接安裝jclasslib。
下載地址:https://github.com/ingokegel/jclasslib/releases
安裝好之後,將class文件放入jclasslib中,可以看到,界面和idea中的界面差不多。
如果由於某些神奇的原因,github上展示無法下載,可以使用本人提供的鏈接:
jclasslib。
3.3 修改class文件字節碼案例
3.3.1 準備對比輸出
因爲修改class文件的代碼使用到了同名的類,爲了不引起混淆,將原代碼的類名改爲ClassFile1,然後main方法添加如下代碼:
public static void main(String[] args) {
System.out.println(J);
}
就是簡單的打印常量J的值運行輸出:2222222。
拿到class文件和源碼放到一個路徑下面。
在包路徑開始處,運行cmd,嘗試使用命令行帶包執行class文件:
可見還是輸出爲2222222
3.3.2 修改class文件
我們此次作簡單的修改,將輸出2222222改成3333。
我們的jclasslib已經提供了修改class文件的API。在jclasslib的安裝目錄中找到lib目錄,將下面的jar包拷貝到項目中:
編寫代碼:
public class ModifyClass {
public static void main(String[] args) throws Exception {
FileInputStream fis = null;
try {
//自己的class文件路徑
String filePath = "J:\\Idea\\jvm\\src\\main\\java\\com\\ikang\\JVM\\classfile\\ClassFile1.class";
fis = new FileInputStream(filePath);
DataInput di = new DataInputStream(fis);
ClassFile cf = new ClassFile();
cf.read(di);
CPInfo[] infos = cf.getConstantPool();
//找到常量池46個位置,CONSTANT_Utf-8_info所以這裏要用這個
ConstantUtf8Info uInfo = (ConstantUtf8Info) infos[46];
//設置新值
uInfo.setBytes("3333".getBytes());
//替換回去
infos[46] = uInfo;
cf.setConstantPool(infos);
File f = new File(filePath);
ClassFileWriter.writeToFile(f, cf);
} finally {
if (fis != null) {
fis.close();
}
}
}
}
爲什麼找到46個位置呢,因爲要修改的J常量的“2222222”字面量值就是存在第46個常量中:
運行之後,再次使用java命令執行原來的class文件:
結果輸出3333,修改成功。
4 附:字節碼指令表
字節碼指令根據功能、屬性不同,可以分爲11大類。下面附上字節碼指令的分類,用於簡單、臨時查看,字節碼指令的詳細介紹,還需要查看官網的介紹。
4.1 Constants 常量相關
十進制 | 操作碼 | 助記符 | 含義 |
00 | 0x00 | nop | 什麼都不做 |
01 | 0x01 | aconst_null | 把 null 推到操作數棧 |
02 | 0x02 | iconst_m1 | 把 int 常量 –1 推到操作數棧 |
03 | 0x03 | iconst_0 | 把 int 常量 0 推到操作數棧 |
04 | 0x04 | iconst_1 | 把 int 常量 1 推到操作數棧 |
05 | 0x05 | iconst_2 | 把 int 常量 2 推到操作數棧 |
06 | 0x06 | iconst_3 | 把 int 常量 3 推到操作數棧 |
07 | 0x07 | iconst_4 | 把 int 常量 4 推到操作數棧 |
08 | 0x08 | iconst_5 | 把 int 常量 5 推到操作數棧 |
09 | 0x09 | lconst_0 | 把 long 常量 0 推到操作數棧 |
10 | 0x0A | lconst_1 | 把 long 常量 1 推到操作數棧 |
11 | 0x0B | fconst_0 | 把 float 常量 0 推到操作數棧 |
12 | 0x0C | fconst_1 | 把 float 常量 1 推到操作數棧 |
13 | 0x0D | fconst_2 | 把 float 常量 2 推到操作數棧 |
14 | 0x0E | dconst_0 | 把 double 常量 0 推到操作數棧 |
15 | 0x0F | dconst_1 | 把 double 常量 1 推到操作數棧 |
16 | 0x10 | bipush | 把單字節常量(-128~127)推到操作數棧 |
17 | 0x11 | sipush | 把 short 常量(-32768~32767)推到操作數棧 |
18 | 0x12 | ldc | 把常量池中的int,float,String型常量取出並推到操作數棧頂 |
19 | 0x13 | ldc_w | 把常量池中的int,float,String型常量取出並推到操作數棧頂(寬索引) |
20 | 0x14 | ldc2_w | 把常量池中的long,double型常量取出並推到操作數棧頂(寬索引) |
4.2 Loads 加載相關
十進制 | 操作碼 | 助記符 | 含義 |
21 | 0x15 | iload | 把 int 型局部變量推到操作數棧 |
22 | 0x16 | lload | 把 long 型局部變量推到操作數棧 |
23 | 0x17 | fload | 把 float 型局部變量推到操作數棧 |
24 | 0x18 | dload | 把 double 型局部變量推到操作數棧 |
25 | 0x19 | aload | 把引用型局部變量推到操作數棧 |
26 | 0x1A | iload_0 | 把局部變量第 1 個 int 型局部變量推到操作數棧 |
27 | 0x1B | iload_1 | 把局部變量第 2 個 int 型局部變量推到操作數棧 |
28 | 0x1C | iload_2 | 把局部變量第 3 個 int 型局部變量推到操作數棧 |
29 | 0x1D | iload_3 | 把局部變量第 4 個 int 型局部變量推到操作數棧 |
30 | 0x1E | lload_0 | 把局部變量第 1 個 long 型局部變量推到操作數棧 |
31 | 0x1F | lload_1 | 把局部變量第 2 個 long 型局部變量推到操作數棧 |
32 | 0x20 | lload_2 | 把局部變量第 3 個 long 型局部變量推到操作數棧 |
33 | 0x21 | lload_3 | 把局部變量第 4 個 long 型局部變量推到操作數棧 |
34 | 0x22 | fload_0 | 把局部變量第 1 個 float 型局部變量推到操作數棧 |
35 | 0x23 | fload_1 | 把局部變量第 2 個 float 型局部變量推到操作數棧 |
36 | 0x24 | fload_2 | 把局部變量第 3 個 float 型局部變量推到操作數棧 |
37 | 0x25 | fload_3 | 把局部變量第 4 個 float 型局部變量推到操作數棧 |
38 | 0x26 | dload_0 | 把局部變量第 1 個 double 型局部變量推到操作數棧 |
39 | 0x27 | dload_1 | 把局部變量第 2 個 double 型局部變量推到操作數棧 |
40 | 0x28 | dload_2 | 把局部變量第 3 個 double 型局部變量推到操作數棧 |
41 | 0x29 | dload_3 | 把局部變量第 4 個 double 型局部變量推到操作數棧 |
42 | 0x2A | aload_0 | 把局部變量第 1 個引用型局部變量推到操作數棧 |
43 | 0x2B | aload_1 | 把局部變量第 2 個引用型局部變量推到操作數棧 |
44 | 0x2C | aload_2 | 把局部變量第 3 個引用型局部變量推到操作數棧 |
45 | 0x2D | aload_3 | 把局部變量第 4 個引用 型局部變量推到操作數棧 |
46 | 0x2E | iaload | 把 int 型數組指定索引的值推到操作數棧 |
47 | 0x2F | laload | 把 long 型數組指定索引的值推到操作數棧 |
48 | 0x30 | faload | 把 float 型數組指定索引的值推到操作數棧 |
49 | 0x31 | daload | 把 double 型數組指定索引的值推到操作數棧 |
50 | 0x32 | aaload | 把引用型數組指定索引的值推到操作數棧 |
51 | 0x33 | baload | 把 boolean或byte型數組指定索引的值推到操作數棧 |
52 | 0x34 | caload | 把 char 型數組指定索引的值推到操作數棧 |
53 | 0x35 | saload | 把 short 型數組指定索引的值推到操作數棧 |
4.3 Store 存儲相關
十進制 | 操作碼 | 助記符 | 含義 |
54 | 0x36 | istore | 把棧頂 int 型數值存入指定局部變量 |
55 | 0x37 | lstore | 把棧頂 long 型數值存入指定局部變量 |
56 | 0x38 | fstore | 把棧頂 float 型數值存入指定局部變量 |
57 | 0x39 | dstore | 把棧頂 double 型數值存入指定局部變量 |
58 | 0x3A | astore | 把棧頂引用型數值存入指定局部變量 |
59 | 0x3B | istore_0 | 把棧頂 int 型數值存入第 1 個局部變量 |
60 | 0x3C | istore_1 | 把棧頂 int 型數值存入第 2 個局部變量 |
61 | 0x3D | istore_2 | 把棧頂 int 型數值存入第 3 個局部變量 |
62 | 0x3E | istore_3 | 把棧頂 int 型數值存入第 4 個局部變量 |
63 | 0x3F | lstore_0 | 把棧頂 long 型數值存入第 1 個局部變量 |
64 | 0x40 | lstore_1 | 把棧頂 long 型數值存入第 2 個局部變量 |
65 | 0x41 | lstore_2 | 把棧頂 long 型數值存入第 3 個局部變量 |
66 | 0x42 | lstore_3 | 把棧頂 long 型數值存入第 4 個局部變量 |
67 | 0x43 | fstore_0 | 把棧頂 float 型數值存入第 1 個局部變量 |
68 | 0x44 | fstore_1 | 把棧頂 float 型數值存入第 2 個局部變量 |
69 | 0x45 | fstore_2 | 把棧頂 float 型數值存入第 3 個局部變量 |
70 | 0x46 | fstore_3 | 把棧頂 float 型數值存入第 4 個局部變量 |
71 | 0x47 | dstore_0 | 把棧頂 double 型數值存入第 1 個局部變量 |
72 | 0x48 | dstore_1 | 把棧頂 double 型數值存入第 2 個局部變量 |
73 | 0x49 | dstore_2 | 把棧頂 double 型數值存入第 3 個局部變量 |
74 | 0x4A | dstore_3 | 把棧頂 double 型數值存入第 4 個局部變量 |
75 | 0x4B | astore_0 | 把棧頂 引用 型數值存入第 1 個局部變量 |
76 | 0x4C | astore_1 | 把棧頂 引用 型數值存入第 2 個局部變量 |
77 | 0x4D | astore_2 | 把棧頂 引用 型數值存入第 3 個局部變量 |
78 | 0x4E | astore_3 | 把棧頂 引用 型數值存入第 4 個局部變量 |
79 | 0x4F | iastore | 把棧頂 int 型數值存入數組指定索引位置 |
80 | 0x50 | lastore | 把棧頂 long 型數值存入數組指定索引位置 |
81 | 0x51 | fastore | 把棧頂 float 型數值存入數組指定索引位置 |
82 | 0x52 | dastore | 把棧頂 double 型數值存入數組指定索引位置 |
83 | 0x53 | aastore | 把棧頂 引用 型數值存入數組指定索引位置 |
84 | 0x54 | bastore | 把棧頂 boolean or byte 型數值存入數組指定索引位置 |
85 | 0x55 | castore | 把棧頂 char 型數值存入數組指定索引位置 |
86 | 0x56 | sastore | 把棧頂 short 型數值存入數組指定索引位置 |
4.4 Stack 棧相關
十進制 | 操作碼 | 助記符 | 含義 |
87 | 0x57 | pop | 把棧頂數值彈出(非long,double數值) |
88 | 0x58 | pop2 | 把棧頂的一個long或double值彈出,或彈出2個其他類型數值 |
89 | 0x59 | dup | 複製棧頂數值並把數值入棧 |
90 | 0x5A | dup_x1 | 複製棧頂數值並將兩個複製值壓入棧頂 |
91 | 0x5B | dup_x2 | 複製棧頂數值並將三個(或兩個)複製值壓入棧頂 |
92 | 0x5C | dup2 | 複製棧頂一個(long 或double 類型的)或兩個(其它)數值並將複製值壓入棧頂 |
93 | 0x5D | dup2_x1 | dup_x1 指令的雙倍版本 |
94 | 0x5E | dup2_x2 | dup_x2 指令的雙倍版本 |
95 | 0x5F | swap | 把棧頂端的兩個數的值交換(數值不能是long 或double 類型< td >的) |
4.5 Math 運算相關
Java 虛擬機在處理浮點數運算時,不會拋出任何運行時異常,當一個操作產生溢出時,將會使用有符號的無窮大來表示,如果某個操作結果沒有明確的數學定義的話,將會使用 NaN 值來表示。所有使用 NaN 值作爲操作數的算術操作,結果都會返回 NaN。
十進制 | 操作碼 | 助記符 | 含義 |
96 | 0x60 | iadd | 把棧頂兩個 int 型數值相加並將結果入棧 |
97 | 0x61 | ladd | 把棧頂兩個 long 型數值相加並將結果入棧 |
98 | 0x62 | fadd | 把棧頂兩個 float 型數值相加並將結果入棧 |
99 | 0x63 | dadd | 把棧頂兩個 double 型數值相加並將結果入棧 |
100 | 0x64 | isub | 把棧頂兩個 int 型數值相減並將結果入棧 |
101 | 0x65 | lsub | 把棧頂兩個 long 型數值相減並將結果入棧 |
102 | 0x66 | fsub | 把棧頂兩個 float 型數值相減並將結果入棧 |
103 | 0x67 | dsub | 把棧頂兩個 double 型數值相減並將結果入棧 |
104 | 0x68 | imul | 把棧頂兩個 int 型數值相乘並將結果入棧 |
105 | 0x69 | lmul | 把棧頂兩個 long 型數值相乘並將結果入棧 |
106 | 0x6A | fmul | 把棧頂兩個 float 型數值相乘並將結果入棧 |
107 | 0x6B | dmul | 把棧頂兩個 double 型數值相乘並將結果入棧 |
108 | 0x6C | idiv | 把棧頂兩個 int 型數值相除並將結果入棧 |
109 | 0x6D | ldiv | 把棧頂兩個 long 型數值相除並將結果入棧 |
110 | 0x6E | fdiv | 把棧頂兩個 float 型數值相除並將結果入棧 |
111 | 0x6F | ddiv | 把棧頂兩個 double 型數值相除並將結果入棧 |
112 | 0x70 | irem | 把棧頂兩個 int 型數值模運算並將結果入棧 |
113 | 0x71 | lrem | 把棧頂兩個 long 型數值模運算並將結果入棧 |
114 | 0x72 | frem | 把棧頂兩個 float 型數值模運算並將結果入棧 |
115 | 0x73 | drem | 把棧頂兩個 double 型數值模運算並將結果入棧 |
116 | 0x74 | ineg | 把棧頂 int 型數值取負並將結果入棧 |
117 | 0x75 | lneg | 把棧頂 long 型數值取負並將結果入棧 |
118 | 0x76 | fneg | 把棧頂 float 型數值取負並將結果入棧 |
119 | 0x77 | dneg | 把棧頂 double 型數值取負並將結果入棧 |
120 | 0x78 | ishl | 把 int 型數左移指定位數並將結果入棧 |
121 | 0x79 | lshl | 把 long 型數左移指定位數並將結果入棧 |
122 | 0x7A | ishr | 把 int 型數右移指定位數並將結果入棧(有符號) |
123 | 0x7B | lshr | 把 long 型數右移指定位數並將結果入棧(有符號) |
124 | 0x7C | iushr | 把 int 型數右移指定位數並將結果入棧(無符號) |
125 | 0x7D | lushr | 把 long 型數右移指定位數並將結果入棧(無符號) |
126 | 0x7E | iand | 把棧頂兩個 int 型數值 按位與 並將結果入棧 |
127 | 0x7F | land | 把棧頂兩個 long 型數值 按位與 並將結果入棧 |
128 | 0x80 | ior | 把棧頂兩個 int 型數值 按位或 並將結果入棧 |
129 | 0x81 | lor | 把棧頂兩個 long 型數值 按或與 並將結果入棧 |
130 | 0x82 | ixor | 把棧頂兩個 int 型數值 按位異或 並將結果入棧 |
131 | 0x83 | lxor | 把棧頂兩個 long 型數值 按位異或 並將結果入棧 |
132 | 0x84 | iinc | 把指定 int 型增加指定值 |
4.6 Conversions 轉換相關
類型轉換指令可以將兩種不同的數值類型進行相互轉換,這些轉換操作一般用於實現用戶代碼中的顯示類型轉換操作。
Java 虛擬機直接支持(即轉換時無需顯示的轉換指令)小範圍類型向大範圍類型的安全轉換,但在處理窄化類型轉換時,必須顯式使用轉換指令來完成。
十進制 | 操作碼 | 助記符 | 含義 |
133 | 0x85 | i2l | 把棧頂 int 強轉 long 併入棧 |
134 | 0x86 | i2f | 把棧頂 int 強轉 float 併入棧 |
135 | 0x87 | i2d | 把棧頂 int 強轉 double 併入棧 |
136 | 0x88 | l2i | 把棧頂 long 強轉 int 併入棧 |
137 | 0x89 | l2f | 把棧頂 long 強轉 float 併入棧 |
138 | 0x8A | l2d | 把棧頂 long 強轉 double 併入棧 |
139 | 0x8B | f2i | 把棧頂 float 強轉 int 併入棧 |
140 | 0x8C | f2l | 把棧頂 float 強轉 long 併入棧 |
141 | 0x8D | f2d | 把棧頂 float 強轉 double 併入棧 |
142 | 0x8E | d2i | 把棧頂 double 強轉 int 併入棧 |
143 | 0x8F | d2l | 把棧頂 double 強轉 long 併入棧 |
144 | 0x90 | d2f | 把棧頂 double 強轉 float 併入棧 |
145 | 0x91 | i2b | 把棧頂 int 強轉 byte 併入棧 |
146 | 0x92 | i2c | 把棧頂 int 強轉 char 併入棧 |
147 | 0x93 | i2s | 把棧頂 int 強轉 short 併入棧 |
4.7 Comparisons 比較相關
十進制 | 操作碼 | 助記符 | 含義 |
148 | 0x94 | lcmp | 比較棧頂兩long 型數值大小,並將結果(1,0,-1)壓入棧頂 |
149 | 0x95 | fcmpl | 比較棧頂兩float 型數值大小,並將結果(1,0,-1)壓入棧頂;當其中一個數值爲“NaN”時,將-1 壓入棧頂 |
150 | 0x96 | fcmpg | 比較棧頂兩float 型數值大小,並將結果(1,0,-1)壓入棧頂;當其中一個數值爲“NaN”時,將1 壓入棧頂 |
151 | 0x97 | dcmpl | 比較棧頂兩double 型數值大小,並將結果(1,0,-1)壓入棧頂;當其中一個數值爲“NaN”時,將-1 壓入棧頂 |
152 | 0x98 | dcmpg | 比較棧頂兩double 型數值大小,並將結果(1,0,-1)壓入棧頂;當其中一個數值爲“NaN”時,將1 壓入棧頂 |
153 | 0x99 | ifeq | 當棧頂 int 型數值等於0時,跳轉 |
154 | 0x9A | ifne | 當棧頂 int 型數值不等於0時,跳轉 |
155 | 0x9B | iflt | 當棧頂 int 型數值小於0時,跳轉 |
156 | 0x9C | ifge | 當棧頂 int 型數值大於等於0時,跳轉 |
157 | 0x9D | ifgt | 當棧頂 int 型數值大於0時,跳轉 |
158 | 0x9E | ifle | 當棧頂 int 型數值小於等於0時,跳轉 |
159 | 0x9F | if_icmpeq | 比較棧頂兩個 int 型數值,等於0時,跳轉 |
160 | 0xA0 | if_icmpne | 比較棧頂兩個 int 型數值,不等於0時,跳轉 |
161 | 0xA1 | if_icmplt | 比較棧頂兩個 int 型數值,小於0時,跳轉 |
162 | 0xA2 | if_icmpge | 比較棧頂兩個 int 型數值,大於等於0時,跳轉 |
163 | 0xA3 | if_icmpgt | 比較棧頂兩個 int 型數值,大於0時,跳轉 |
164 | 0xA4 | if_icmple | 比較棧頂兩個 int 型數值,小於等於0時,跳轉 |
165 | 0xA5 | if_acmpeq | 比較棧頂兩個 引用 型數值,相等時跳轉 |
166 | 0xA6 | if_acmpne | 比較棧頂兩個 引用 型數值,不相等時跳轉 |
4.8 Control 控制相關
控制轉移指令可以讓 Java 虛擬機有條件或無條件地從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程序,從概念模型上理解,可以認爲控制轉移指令就是在有條件或無條件地修改 PC 寄存器的值。
十進制 | 操作碼 | 助記符 | 含義 |
167 | 0xA7 | goto | 無條件分支跳轉 |
168 | 0xA8 | jsr | 跳轉至指定16 位offset(bit) 位置,並將jsr 下一條指令地址壓入棧頂 |
169 | 0xA9 | ret | 返回至局部變量指定的index 的指令位置(一般與jsr,jsr_w聯合使用) |
170 | 0xAA | tableswitch | 用於switch 條件跳轉,case 值連續(可變長度指令) |
171 | 0xAB | lookupswitch | 用於switch 條件跳轉,case 值不連續(可變長度指令) |
172 | 0xAC | ireturn | 結束方法,並返回一個int 類型數據 |
173 | 0xAD | lreturn | 從當前方法返回 long |
174 | 0xAE | freturn | 從當前方法返回 float |
175 | 0xAF | dreturn | 從當前方法返回 double |
176 | 0xB0 | areturn | 從當前方法返回 對象引用 |
177 | 0xB1 | return | 從當前方法返回 void |
4.9 references 引用、方法、異常、同步相關
十進制 | 操作碼 | 助記符 | 含義 |
178 | 0xB2 | getstatic | 獲取指定類的靜態域,並將其值壓入棧頂 |
179 | 0xB3 | putstatic | 爲類的靜態域賦值 |
180 | 0xB4 | getfield | 獲取指定類的實例域(對象的字段值),並將其值壓入棧頂 |
181 | 0xB5 | putfield | 爲指定的類的實例域賦值 |
182 | 0xB6 | invokevirtual | 調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),是Java語言中最常見的方法分派方式。 |
183 | 0xB7 | invokespecial | 調用一些需要特殊處理的實例方法,包括實例初始化方法()、私有方法和父類方法。這三類方法的調用對象在編譯時就可以確定。 |
184 | 0xB8 | invokestatic | 調用靜態方法 |
185 | 0xB9 | invokeinterface | 調用接口方法調,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。 |
186 | 0xBA | invokedynamic | 調用動態鏈接方法(該指令是指令是Java SE 7 中新加入的)。用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法,前面4條調用指令的分派邏輯都固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。 |
187 | 0xBB | new | 創建一個對象,並將其引用值壓入棧頂 |
188 | 0xBC | newarray | 創建一個指定原始類型(如int、float、char……)的數組,並將其引用值壓入棧頂 |
189 | 0xBD | anewarray | 創建一個引用型(如類,接口,數組)的數組,並將其引用值壓入棧頂 |
190 | 0xBE | arraylength | 獲得數組的長度值並壓入棧頂 |
191 | 0xBF | athrow | 將棧頂的異常直接拋出。Java程序中顯式拋出異常的操作(throw語句)都由athrow指令來實現,並且,在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的,而是採用異常表來完成的。 |
192 | 0xC0 | checkcast | 檢驗類型轉換,檢驗未通過將拋出ClassCastException |
193 | 0xC1 | instanceof | 檢驗對象是否是指定的類的實例,如果是將1 壓入棧頂,否則將0 壓入棧頂 |
194 | 0xC2 | monitorenter | 獲取對象的monitor,用於同步塊或同步方法 |
195 | 0xC3 | monitorexit | 釋放對象的monitor,用於同步塊或同步方法 |
Java 虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支持的。
**方法級的同步是隱式的,即無須通過字節碼指令來控制,它實現在方法調用和返回操作之中。**虛擬機可以從方法常量池的方法表結構中的 ACC_SYNCHRONIZED 方法標誌得知一個方法是否聲明爲同步方法。當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程就要求先成功持有管程,然後才能執行方法,最後當方法完成(無論是正常完成還是非正常完成)時釋放管程。在方法執行期間,執行線程持有了管程,其他任何線程都無法再獲取到同一個管程。如果一個同步方法執行期間拋出了異常,並且在方法內部無法處理此異常,那麼這個 同步方法所持有的管程將在異常拋到同步方法之外時自動釋放。
同步一段指令集序列通常是由Java語言中的synchronized語句塊來表示的,Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義
編譯器必須確保無論方法通過何種方式完成,方法中調用過的每條monitorenter指令都必須執行其對應的monitorexit指令,而無論這個方法是正常結束還是異常結束。
4.10 Extended 擴展相關
十進制 | 操作碼 | 助記符 | 含義 |
196 | 0xC4 | wide | 擴展訪問局部變量表的索引寬度 |
197 | 0xC5 | multianewarray | 創建指定類型和指定維度的多維數組(執行該指令時,操作棧中必須包含各維度的長度值),並將其引用值壓入棧頂 |
198 | 0xC6 | ifnull | 爲 null 時跳轉 |
199 | 0xC7 | ifnonnull | 非 null 時跳轉 |
200 | 0xC8 | goto_w | 無條件跳轉(寬索引) |
201 | 0xC9 | jsr_w | 跳轉指定32bit偏移位置,並將jsr_w下一條指令地址入棧 |
4.11 Reserved 保留指令
十進制 | 操作碼 | 助記符 | 含義 |
202 | 0xCA | breakpoint | 調試時的斷點 |
254 | 0xFE | impdep1 | 用於在特定硬件中使用的語言後門 |
255 | 0xFF | impdep2 | 用於在特定硬件中使用的語言後門 |
5 參考和學習
JVM規範 Java SE8官方文檔
JVM規範中《操作碼助記符表》
JVM規範中《JVM指令集》介紹(包括操作碼對應的操作數)
《Java虛擬機規範》
《深入理解Java虛擬機》
《實戰Java虛擬機》
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!