Java 字節碼指令是 JVM 體系中很是難啃的一塊硬骨頭,我估計有些讀者會有這樣的疑惑,「Java 字節碼難學嗎?我能不能學會啊?」java
講良心話,不是我謙虛,一開始學 Java 字節碼和 Java 虛擬機方面的知識我也感受頭大!但硬着頭皮學了一陣子以後,忽然就開竅了,以爲好有意思,尤爲是明白了 Java 代碼在底層居然是這樣執行的時候,感受既膨脹又飄飄然,渾身上下散發着自信的光芒!git
我在 掘金 共輸出了 100 多篇 Java 方面的文章,總字數超過 30 萬字, 內容風趣幽默、通俗易懂,收穫了不少初學者的承認和支持,內容包括 Java 語法、Java 集合框架、Java 併發編程、Java 虛擬機等核心內容。 爲了幫助更多的 Java 初學者,我「一怒之下」就把這些文章從新整理並開源到了 GitHub,起名《教妹學 Java》,聽起來是否是就頗有趣?github
GitHub 開源地址(歡迎 star):github.com/itwanger/jm…編程
Java 官方的虛擬機 Hotspot 是基於棧的,而不是基於寄存器的。segmentfault
基於棧的優勢是可移植性更好、指令更短、實現起來簡單,但不能隨機訪問棧中的元素,完成相同功能所須要的指令數也比寄存器的要多,須要頻繁的入棧和出棧。數組
基於寄存器的優勢是速度快,有利於程序運行速度的優化,但操做數須要顯式指定,指令也比較長。markdown
Java 字節碼由操做碼和操做數組成。併發
因爲 Java 虛擬機是基於棧而不是寄存器的結構,因此大多數指令都只有一個操做碼。好比 aload_0
(將局部變量表中下標爲 0 的數據壓入操做數棧中)就只有操做碼沒有操做數,而 invokespecial #1
(調用成員方法或者構造方法,並傳遞常量池中下標爲 1 的常量)就是由操做碼和操做數組成的。框架
加載(load)和存儲(store)相關的指令是使用最頻繁的指令,用於將數據從棧幀的局部變量表和操做數棧之間來回傳遞。jvm
1)將局部變量表中的變量壓入操做數棧中
解釋一下。
x 爲操做碼助記符,代表是哪種數據類型。見下表所示。
像 arraylength 指令,沒有操做碼助記符,它沒有表明數據類型的特殊字符,但操做數只能是一個數組類型的對象。
大部分的指令都不支持 byte、short 和 char,甚至沒有任何指令支持 boolean 類型。編譯器會將 byte 和 short 類型的數據帶符號擴展(Sign-Extend)爲 int 類型,將 boolean 和 char 零位擴展(Zero-Extend)爲 int 類型。
舉例來講。
private void load(int age, String name, long birthday, boolean sex) {
System.out.println(age + name + birthday + sex);
}
複製代碼
經過 jclasslib 看一下 load()
方法(4 個參數)的字節碼指令。
經過查看局部變量表就能關聯上了。
2)將常量池中的常量壓入操做數棧中
根據數據類型和入棧內容的不一樣,此類又能夠細分爲 const 系列、push 系列和 Idc 指令。
const 系列,用於特殊的常量入棧,要入棧的常量隱含在指令自己。
push 系列,主要包括 bipush 和 sipush,前者接收 8 位整數做爲參數,後者接收 16 位整數。
Idc 指令,當 const 和 push 不能知足的時候,萬能的 Idc 指令就上場了,它接收一個 8 位的參數,指向常量池中的索引。
Idc_w
:接收兩個 8 位數,索引範圍更大。Idc2_w
指令。舉例來講。
public void pushConstLdc() {
// 範圍 [-1,5]
int iconst = -1;
// 範圍 [-128,127]
int bipush = 127;
// 範圍 [-32768,32767]
int sipush= 32767;
// 其餘 int
int ldc = 32768;
String aconst = null;
String IdcString = "沉默王二";
}
複製代碼
經過 jclasslib 看一下 pushConstLdc()
方法的字節碼指令。
3)將棧頂的數據出棧並裝入局部變量表中
主要是用來給局部變量賦值,這類指令主要以 store 的形式存在。
明白了 xload_ 和 xload,再看 xstore_ 和 xstore 就會輕鬆得多,做用反了一下而已。
你們來想一個問題,爲何要有 xstore_ 和 xload_ 呢?它們的做用和 xstore n、xload n 不是同樣的嗎?
xstore_ 和 xstore n 的區別在於,前者至關於只有操做碼,佔用 1 個字節;後者至關於由操做碼和操做數組成,操做碼佔 1 個字節,操做數佔 2 個字節,一共佔 3 個字節。
因爲局部變量表中前幾個位置老是很是經常使用,雖然 xstore_<n>
和 xload_<n>
增長了指令數量,但字節碼的體積變小了!
舉例來講。
public void store(int age, String name) {
int temp = age + 2;
String str = name;
}
複製代碼
經過 jclasslib 看一下 store()
方法的字節碼指令。
經過查看局部變量表就能關聯上了。
算術指令用於對兩個操做數棧上的值進行某種特定運算,並把結果從新壓入操做數棧。能夠分爲兩類:整型數據的運算指令和浮點數據的運算指令。
須要注意的是,數據運算可能會致使溢出,好比兩個很大的正整數相加,極可能會獲得一個負數。但 Java 虛擬機規範中並無對這種狀況給出具體結果,所以程序是不會顯式報錯的。因此,你們在開發過程當中,若是涉及到較大的數據進行加法、乘法運算的時候,必定要注意!
當發生溢出時,將會使用有符號的無窮大 Infinity 來表示;若是某個操做結果沒有明確的數學定義的話,將會使用 NaN 值來表示。並且全部使用 NaN 做爲操做數的算術操做,結果都會返回 NaN。
舉例來講。
public void infinityNaN() {
int i = 10;
double j = i / 0.0;
System.out.println(j); // Infinity
double d1 = 0.0;
double d2 = d1 / 0.0;
System.out.println(d2); // NaN
}
複製代碼
Java 虛擬機提供了兩種運算模式:
我把全部的算術指令列一下:
舉例來講。
public void calculate(int age) {
int add = age + 1;
int sub = age - 1;
int mul = age * 2;
int div = age / 3;
int rem = age % 4;
age++;
age--;
}
複製代碼
經過 jclasslib 看一下 calculate()
方法的字節碼指令。
能夠分爲兩種:
1)寬化,小類型向大類型轉換,好比 int–>long–>float–>double
,對應的指令有:i2l、i2f、i2d、l2f、l2d、f2d。
2)窄化,大類型向小類型轉換,好比從 int 類型到 byte、short 或者 char,對應的指令有:i2b、i2s、i2c;從 long 到 int,對應的指令有:l2i;從 float 到 int 或者 long,對應的指令有:f2i、f2l;從 double 到 int、long 或者 float,對應的指令有:d2i、d2l、d2f。
舉例來講。
public void updown() {
int i = 10;
double d = i;
float f = 10f;
long ong = (long)f;
}
複製代碼
經過 jclasslib 看一下 updown()
方法的字節碼指令。
Java 是一門面向對象的編程語言,那麼 Java 虛擬機是如何從字節碼層面進行支持的呢?
1)建立指令
數組也是一種對象,但它建立的字節碼指令和普通的對象不一樣。建立數組的指令有三種:
普通對象的建立指令只有一個,就是 new
,它會接收一個操做數,指向常量池中的一個索引,表示要建立的類型。
舉例來講。
public void newObject() {
String name = new String("沉默王二");
File file = new File("無愁河的浪蕩漢子.book");
int [] ages = {};
}
複製代碼
經過 jclasslib 看一下 newObject()
方法的字節碼指令。
new #13 <java/lang/String>
,建立一個 String 對象。new #15 <java/io/File>
,建立一個 File 對象。newarray 10 (int)
,建立一個 int 類型的數組。2)字段訪問指令
字段能夠分爲兩類,一類是成員變量,一類是靜態變量(static 關鍵字修飾的),因此字段訪問指令能夠分爲兩類:
舉例來講。
public class Writer {
private String name;
static String mark = "做者";
public static void main(String[] args) {
print(mark);
Writer w = new Writer();
print(w.name);
}
public static void print(String arg) {
System.out.println(arg);
}
}
複製代碼
經過 jclasslib 看一下 main()
方法的字節碼指令。
getstatic #2 <com/itwanger/jvm/Writer.mark>
,訪問靜態變量 markgetfield #6 <com/itwanger/jvm/Writer.name>
,訪問成員變量 name方法調用指令有 5 個,分別用於不一樣的場景:
舉例來講。
public class InvokeExamples {
private void run() {
List ls = new ArrayList();
ls.add("難頂");
ArrayList als = new ArrayList();
als.add("學不動了");
}
public static void print() {
System.out.println("invokestatic");
}
public static void main(String[] args) {
print();
InvokeExamples invoke = new InvokeExamples();
invoke.run();
}
}
複製代碼
咱們用 javap -c InvokeExamples.class
來反編譯一下。
Compiled from "InvokeExamples.java"
public class com.itwanger.jvm.InvokeExamples {
public com.itwanger.jvm.InvokeExamples();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
private void run();
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String 難頂
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: new #2 // class java/util/ArrayList
20: dup
21: invokespecial #3 // Method java/util/ArrayList."<init>":()V
24: astore_2
25: aload_2
26: ldc #6 // String 學不動了
28: invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
31: pop
32: return
public static void print();
Code:
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #9 // String invokestatic
5: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #11 // Method print:()V
3: new #12 // class com/itwanger/jvm/InvokeExamples
6: dup
7: invokespecial #13 // Method "<init>":()V
10: astore_1
11: aload_1
12: invokevirtual #14 // Method run:()V
15: return
}
複製代碼
InvokeExamples 類有 4 個方法,包括缺省的構造方法在內。
1)InvokeExamples()
構造方法中
缺省的構造方法內部會調用超類 Object 的初始化構造方法:
`invokespecial #1 // Method java/lang/Object."<init>":()V`
複製代碼
2)成員方法 run()
中
invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
複製代碼
因爲 ls 變量的引用類型爲接口 List,因此 ls.add()
調用的是 invokeinterface
指令,等運行時再肯定是否是接口 List 的實現對象 ArrayList 的 add()
方法。
invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
複製代碼
因爲 als 變量的引用類型已經肯定爲 ArrayList,因此 als.add()
方法調用的是 invokevirtual
指令。
3)main()
方法中
invokestatic #11 // Method print:()V
複製代碼
print()
方法是靜態的,因此調用的是 invokestatic
指令。
方法返回指令根據方法的返回值類型進行區分,常見的返回指令見下圖。
常見的操做數棧管理指令有 pop、dup 和 swap。
這些指令不須要指明數據類型,由於是按照位置壓入和彈出的。
舉例來講。
public class Dup {
int age;
public int incAndGet() {
return ++age;
}
}
複製代碼
經過 jclasslib 看一下 incAndGet()
方法的字節碼指令。
控制轉移指令包括:
1)比較指令
比較指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp,指令的第一個字母表明的含義分別是 double、float、long。注意,沒有 int 類型。
對於 double 和 float 來講,因爲 NaN 的存在,有兩個版本的比較指令。拿 float 來講,有 fcmpg 和 fcmpl,區別在於,若是遇到 NaN,fcmpg 會將 1 壓入棧,fcmpl 會將 -1 壓入棧。
舉例來講。
public void lcmp(long a, long b) {
if(a > b){}
}
複製代碼
經過 jclasslib 看一下 lcmp()
方法的字節碼指令。
lcmp 用於兩個 long 型的數據進行比較。
2)條件跳轉指令
這些指令都會接收兩個字節的操做數,它們的統一含義是,彈出棧頂元素,測試它是否知足某一條件,知足的話,跳轉到對應位置。
對於 long、float 和 double 類型的條件分支比較,會先執行比較指令返回一個整形值到操做數棧中後再執行 int 類型的條件跳轉指令。
對於 boolean、byte、char、short,以及 int,則直接使用條件跳轉指令來完成。
舉例來講。
public void fi() {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
複製代碼
經過 jclasslib 看一下 fi()
方法的字節碼指令。
3 ifne 12 (+9)
的意思是,若是棧頂的元素不等於 0,跳轉到第 12(3+9)行 12 bipush 20
。
3)比較條件轉指令
前綴「if_」後,以字符「i」開頭的指令針對 int 型整數進行操做,以字符「a」開頭的指令表示對象的比較。
舉例來講。
public void compare() {
int i = 10;
int j = 20;
System.out.println(i > j);
}
複製代碼
經過 jclasslib 看一下 compare()
方法的字節碼指令。
11 if_icmple 18 (+7)
的意思是,若是棧頂的兩個 int 類型的數值比較的話,若是前者小於後者時跳轉到第 18 行(11+7)。
4)多條件分支跳轉指令
主要有 tableswitch 和 lookupswitch,前者要求多個條件分支值是連續的,它內部只存放起始值和終止值,以及若干個跳轉偏移量,經過給定的操做數 index,能夠當即定位到跳轉偏移量位置,所以效率比較高;後者內部存放着各個離散的 case-offset 對,每次執行都要搜索所有的 case-offset 對,找到匹配的 case 值,並根據對應的 offset 計算跳轉地址,所以效率較低。
舉例來講。
public void switchTest(int select) {
int num;
switch (select) {
case 1:
num = 10;
break;
case 2:
case 3:
num = 30;
break;
default:
num = 40;
}
}
複製代碼
經過 jclasslib 看一下 switchTest()
方法的字節碼指令。
case 2 的時候沒有 break,因此 case 2 和 case 3 是連續的,用的是 tableswitch。若是等於 1,跳轉到 28 行;若是等於 2 和 3,跳轉到 34 行,若是是 default,跳轉到 40 行。
5)無條件跳轉指令
goto 指令接收兩個字節的操做數,共同組成一個帶符號的整數,用於指定指令的偏移量,指令執行的目的就是跳轉到偏移量給定的位置處。
前面的例子裏都出現了 goto 的身影,也很好理解。若是指令的偏移量特別大,超出了兩個字節的範圍,可使用指令 goto_w,接收 4 個字節的操做數。
巨人的肩膀:
除了以上這些指令,還有異常處理指令和同步控制指令,我打算吊一吊你們的胃口,你們能夠期待一波~~
(騷操做)
路漫漫其修遠兮,吾將上下而求索
想要走得更遠,Java 字節碼這塊就必須得硬碰硬地吃透,但願二哥的這些分享能夠幫助到你們~
二哥在 掘金 上寫了不少 Java 方面的系列文章,有 Java 核心語法、Java 集合框架、Java IO、Java 併發編程、Java 虛擬機等,也算是體系完整了。
爲了能幫助到更多的 Java 初學者,二哥把本身連載的《教妹學Java》開源到了 GitHub,儘管只整理了 50 篇,發現字數已經來到了 10 萬+,內容更是沒得說,通俗易懂、風趣幽默、圖文並茂。
GitHub 開源地址(歡迎 star):github.com/itwanger/jm…
若是有幫助的話,還請給二哥點個贊,這將是我繼續分享下去的最強動力!