天天看點

為什麼要推薦大家學習位元組碼? 配套視訊:一、背景二、為什麼要學習位元組碼三、舉例四、總結

 配套視訊:

為什麼推薦大家學習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的一些文法(篇幅有限,而且例子太多,這裡就不給出了,感興趣的同學自己嘗試),甚至幫助大家學習源碼。

試想一下,如果你認為學習位元組碼無用,甚至你都不了解,你怎麼可能用它來解決問題呢?

你所掌握的知識幫助你成長由限制了你的成長,要敢于突破舒适區,給自己更多的成長機會。

-------------------

歡迎點贊、評論、轉發,你的鼓勵,是我創作的動力。