爲什麼要推薦大家學習字節碼?

配套視頻:

爲什麼推薦大家學習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 指令,慢慢就熟悉了,另外強力推薦jclasslib bytecode viewer插件,該插件可以點擊指令跳轉到 Java虛擬機規範對該指令的介紹的部分,對學習幫助極大。

很多人可能會說,學這個太慢。

的確,急於求成怎麼能學的特別好呢?厚積才能薄發,耐不住寂寞怎麼能學有所成呢。

本文通過這其中一個例子讓大家理解,JVM字節碼可以幫助大家理解Java的一些語法(篇幅有限,而且例子太多,這裏就不給出了,感興趣的同學自己嘗試),甚至幫助大家學習源碼。

試想一下,如果你認爲學習字節碼無用,甚至你都不瞭解,你怎麼可能用它來解決問題呢?

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

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

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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章