現在的位置: 首頁 > 綜合 > 正文

為什麼要學習位元組碼

2020年02月24日 綜合 ⁄ 共 5977字 ⁄ 字型大小 評論關閉

  一、背景

  可能很多人會覺得沒必要,因為平時開發用不到,而且不學這個也沒耽誤學習。但是這裡分享一點感悟,即人總是根據自己已經掌握的知識和技能來解決問題的。

  這裡有個悖論,有時候你覺得有些技術沒用恰恰是因為你沒有熟練掌握它,遇到可以使用它的場景你根本想不到用。

  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 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."":()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."":()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 data = new ArrayList<>();

  public void someMethod(String param) {

  List 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."":()V

  4: aload_0

  5: new #2 // class java/util/ArrayList

  8: dup

  9: invokespecial #3 // Method java/util/ArrayList."":()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 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."":()V

  4: aload_0

  5: new #2 // class java/util/ArrayList

  8: dup

  9: invokespecial #3 // Method java/util/ArrayList."":()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。

  3.3 分析

  通過源碼其實我們並不能很好的理解到底優化了哪裡。

  我們分別對兩個類進行編譯和反彙編後可以清晰地看到:第一個例子代碼多了一行,反而反編譯後的位元組碼更短。

  第二個例子反編譯後的位元組碼比第一個例子長在哪裡呢?

  我們發現主要多在:getfield #4 // Field data:Ljava/util/List; 這裡。

  即每次獲取 data對象都要先 aload_0 然後再 getfield 指令獲取。

  第一個例子通過 astore_2 將其存到了局部變數表中,每次用直接 aload_2 直接從局部變數表中載入到操作數棧。

  從而不需要每次都從 this 對象中獲取這個屬性,因此效率更高。

  這種思想有點像寫代碼中常用的緩存,即將最近要使用的數據先查一次緩存起來,使用時優先查緩存。

  本質上體現了操作系統中的時間局部性和空間局部性的概念(不懂的話翻下書或百度下)。

  因此通過位元組碼的分析,通過聯繫實際的開發經驗,為什麼要學習位元組碼通過聯繫專業知識,這個問題我們就搞明白了。

  另外也體現了用空間換時間的思想。知識只有能貫穿起來,理解的才能更牢固。此處也體現出專業基礎的重要性。

  另外知識能聯繫起來、思考到本質,理解才能更深刻,記憶才能更牢固,才更有可能靈活運用。

  四、總結

  這只是其中一個非常典型的例子,學習 JVM 位元組碼能夠給你一個不一樣的視角,讓你多一個學習的途徑。

抱歉!評論已關閉.