配套視訊:
為什麼推薦大家學習Java位元組碼
https://www.bilibili.com/video/av77600176/一、背景
本文主要探讨:為什麼要學習 JVM 位元組碼?
可能很多人會覺得沒必要,因為平時開發用不到,而且不學這個也沒耽誤學習。
但是這裡分享一點感悟,即人總是根據自己已經掌握的知識和技能來解決問題的。
這裡有個悖論,有時候你覺得有些技術沒用恰恰是因為你沒有熟練掌握它,遇到可以使用它的場景你根本想不到用。
1.1 從生活的角度來講
如果你是一個非計算機專業的學生,你老師給你幾張圖書的拍照,大概3000字,讓你列印成文字。
你打開電腦,噼裡啪啦一頓敲,搞了一下午幹完了。
如果你知道語音輸入,那麼你可能采用語音輸入的方式,30分鐘搞定。
如果你了解 OCR 圖檔文字識别,可能 5 分鐘搞定。
不同的方法,帶來的效果完全不同。然而最可怕的是,你不會語音輸入或者OCR你不會覺得自己少了啥。
OCR識别絕對不是你提高點打字速度可以追趕上的。
1.2 學習Java的角度
很多人學習知識主要依賴百度,依賴部落格,依賴視訊和圖書,而且這些資料品質參差不齊,而且都是别人了解之後的結果。
比如你平時不怎麼看源碼,那麼你就很少能将源碼作為你學習的素材,隻能依賴部落格、圖書、視訊等。
如果你平時喜歡看源碼,你會對源碼有自己的了解,你會發現源碼對你的學習有很多幫助。
如果你平時不怎麼用反編譯和反彙編,那麼你更多地隻能依賴源碼,依賴調試等學習知識,而不能從位元組碼層面來學習和了解知識。
當你慢慢熟練讀懂虛拟機指令,你會發現你多了一個學習知識的途徑。
二、為什麼要學習位元組碼
2.1 人總是不願意離開舒适區的
很多人在學習新知識時,總是本能地抵觸。會找各種理由不去學,“比如暫時用不到”,“學了沒啥用”,“以後再說”。
甚至認為這是在浪費時間。
2.2 為什麼要學習位元組碼?
最近學習了一段時間 JVM 位元組碼的知識,雖然不算精通,但是讀位元組碼起來已經不太吃力。
為什麼推薦學習位元組碼是因為它可以從比源碼更深的層面去學習 Java 相關知識。
雖然不可能所有問題都用位元組碼的知識來解決,但是它給你一個學習的途徑。
比如通過位元組碼的學習你可以更好地了解 Java中各種文法和文法糖背後的原理,更好地了解多态等語言特性。
三、舉例
本文舉一個簡單的例子,來說明學習位元組碼的作用。
3.1 例子
3.1.1 文法糖
public class ForEachDemo {
public static void main(String[] args) {
List<String> data = new ArrayList<>();
data.add("a");
data.add("b");
for (String str : data) {
System.out.println(str);
}
}
}
編譯: javac ForEachDemo.java
反彙編:javap -c ForEachDemo
public class com.imooc.basic.learn_source_code.local.ForEachDemo {
public com.imooc.basic.learn_source_code.local.ForEachDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
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 a
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #6 // String b
20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_2
33: aload_2
34: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
39: ifeq 62
42: aload_2
43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
48: checkcast #10 // class java/lang/String
51: astore_3
52: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
55: aload_3
56: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
59: goto 33
62: return
}
我們可以清晰地看到foreach 循環底層用到了疊代器實作,甚至可以逆向腦補出對應的Java源碼(大家可以嘗試根據位元組碼寫出等價的源碼)。
3.1.2 讀源碼遇到的一個問題
我們在讀源碼時經常會遇到類似下面的這種寫法:
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#startWebServer
private WebServer startWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.start();
}
return webServer;
}
在函數中聲明一個和成員變量同名的局部變量,然後将成員變量指派給局部變量,再去使用。
看似很小的細節,隐含着一個優化思想。
可能有些人讀過某些文章有提到(可是為什麼我們總得看到一個文章會一個知識?如果沒看到怎麼辦?),更多的人可能并不能了解有什麼優化。
3.2 模拟
普通的文法糖這裡就不做過多展開,重點講講第二個優化的例子。
模仿上述寫法的例子:
public class LocalDemo {
private List<String> data = new ArrayList<>();
public void someMethod(String param) {
List<String> data = this.data;
if (data != null && data.size() > 0 && data.contains(param)) {
System.out.println(data.indexOf(param));
}
}
}
編譯:javac LocalDemo.java
反彙編: javap -c LocalDemo
public class com.imooc.basic.learn_source_code.local.LocalDemo {
public com.imooc.basic.learn_source_code.local.LocalDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/ArrayList
8: dup
9: invokespecial #3 // Method java/util/ArrayList."<init>":()V
12: putfield #4 // Field data:Ljava/util/List;
15: return
public void someMethod(java.lang.String);
Code:
0: aload_0
1: getfield #4 // Field data:Ljava/util/List;
4: astore_2
5: aload_2
6: ifnull 41
9: aload_2
10: invokeinterface #5, 1 // InterfaceMethod java/util/List.size:()I
15: ifle 41
18: aload_2
19: aload_1
20: invokeinterface #6, 2 // InterfaceMethod java/util/List.contains:(Ljava/lang/Object;)Z
25: ifeq 41
28: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
31: aload_2
32: aload_1
33: invokeinterface #8, 2 // InterfaceMethod java/util/List.indexOf:(Ljava/lang/Object;)I
38: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
41: return
}
此時 局部變量表中 0 為 this , 1 為 param 2 為 局部變量 data
直接使用成員變量的例子:
public class ThisDemo {
private List<String> data = new ArrayList<>();
public void someMethod(String param) {
if (data != null && data.size() > 0 && data.contains(param)) {
System.out.println(data.indexOf(param));
}
}
}
編譯:javac ThisDemo.java
反彙編: javap -c ThisDemo
public class com.imooc.basic.learn_source_code.local.ThisDemo {
public com.imooc.basic.learn_source_code.local.ThisDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/ArrayList
8: dup
9: invokespecial #3 // Method java/util/ArrayList."<init>":()V
12: putfield #4 // Field data:Ljava/util/List;
15: return
public void someMethod(java.lang.String);
Code:
0: aload_0
1: getfield #4 // Field data:Ljava/util/List;
4: ifnull 48
7: aload_0
8: getfield #4 // Field data:Ljava/util/List;
11: invokeinterface #5, 1 // InterfaceMethod java/util/List.size:()I
16: ifle 48
19: aload_0
20: getfield #4 // Field data:Ljava/util/List;
23: aload_1
24: invokeinterface #6, 2 // InterfaceMethod java/util/List.contains:(Ljava/lang/Object;)Z
29: ifeq 48
32: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
35: aload_0
36: getfield #4 // Field data:Ljava/util/List;
39: aload_1
40: invokeinterface #8, 2 // InterfaceMethod java/util/List.indexOf:(Ljava/lang/Object;)I
45: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
48: return
}
此時局部變量表隻有兩個,即 this 和 param。
大家也可以通過 javap -c -v 來檢視更詳細資訊,本例截圖中用到 IDEA 插件為
jclasslib bytecode viewer,感興趣參考我的另外一篇對該工具的介紹博文:《
IDEA位元組碼學習檢視神器jclasslib bytecode viewer介紹》。
3.3 分析
通過源碼其實我們并不能很好的了解到底優化了哪裡。
我們分别對兩個類進行編譯和反彙編後可以清晰地看到:第一個例子代碼多了一行,反而反編譯後的位元組碼更短。
第二個例子反編譯後的位元組碼比第一個例子長在哪裡呢?
我們發現主要多在:getfield #4 // Field data:Ljava/util/List; 這裡。
即每次擷取 data對象都要先 aload_0 然後再 getfield 指令擷取。
第一個例子通過 astore_2 将其存到了局部變量表中,每次用直接 aload_2 直接從局部變量表中加載到操作數棧。
進而不需要每次都從 this 對象中擷取這個屬性,是以效率更高。
這種思想有點像寫代碼中常用的緩存,即将最近要使用的資料先查一次緩存起來,使用時優先查緩存。
本質上展現了作業系統中的時間局部性和空間局部性的概念(不懂的話翻下書或百度下)。
是以通過位元組碼的分析,通過聯系實際的開發經驗,通過聯系專業知識,這個問題我們就搞明白了。
另外也展現了用空間換時間的思想。
知識隻有能貫穿起來,了解的才能更牢固。
此處也展現出專業基礎的重要性。
另外知識能聯系起來、思考到本質,了解才能更深刻,記憶才能更牢固,才更有可能靈活運用。
四、總結
這隻是其中一個非常典型的例子,學習 JVM 位元組碼能夠給你一個不一樣的視角,讓你多一個學習的途徑。
可能很多人說自己想學但是無從下手,這裡推薦大家先看《深入了解Java虛拟機》,然後結合《Java虛拟機規範》,平時多敲一下 javap 指令,慢慢就熟悉了,另外強力推薦
插件,該插件可以點選指令跳轉到 Java虛拟機規範對該指令的介紹的部分,對學習幫助極大。
很多人可能會說,學這個太慢。
的确,急于求成怎麼能學的特别好呢?厚積才能薄發,耐不住寂寞怎麼能學有所成呢。
本文通過這其中一個例子讓大家了解,JVM位元組碼可以幫助大家了解Java的一些文法(篇幅有限,而且例子太多,這裡就不給出了,感興趣的同學自己嘗試),甚至幫助大家學習源碼。
試想一下,如果你認為學習位元組碼無用,甚至你都不了解,你怎麼可能用它來解決問題呢?
你所掌握的知識幫助你成長由限制了你的成長,要敢于突破舒适區,給自己更多的成長機會。
-------------------
歡迎點贊、評論、轉發,你的鼓勵,是我創作的動力。