從1+1=2來理解Java位元組碼

a1322674015發表於2019-12-31

背景

前不久《深入理解Java虛擬機器》第三版釋出了,趕緊買來看了看新版的內容,這本書更新了很多新版本虛擬機器的內容,還對以前的部分內容進行了重構,還是值得去看的。本著複習和鞏固的態度,我決定來編譯一個簡單的類檔案來分析Java的位元組碼內容,來幫助理解和鞏固Java位元組碼知識,希望也對閱讀本文的你有所幫助。

說明:本次採用的環境是OpenJdk12

編譯“1+1”程式碼

首先我們需要寫個簡單的小程式,1+1的程式,學習就要從最簡單的1+1開始,程式碼如下:

package top.luozhou.test;/**
 * @description:
 * @author: luozhou
 * @create: 2019-12-25 21:28
 **/public class TestJava {    
     public static void main(String[] args) {
         int a=1+1;
        System.out.println(a);
    }
}

寫好java類檔案後,首先執行命令javac TestJava.java 編譯類檔案,生成TestJava.class。 然後執行反編譯命令javap -verbose TestJava,位元組碼結果顯示如下:

  Compiled from "TestJava.java"public class top.luozhou.test.TestJava
  minor version: 0
  major version: 56
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // top/luozhou/test/TestJava
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               TestJava.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               top/luozhou/test/TestJava
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V
{  public top.luozhou.test.TestJava();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:      stack=1, locals=1, args_size=1
         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:      stack=2, locals=2, args_size=1
         0: iconst_2         1: istore_1         2: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1         6: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 9}

解析位元組碼

1.基礎資訊

上述結果刪除了部分不影響解析的冗餘資訊,接下來我們便來解析位元組碼的結果。

 minor version: 0 次版本號,為0表示未使用
 major version: 56 主版本號,56表示jdk12,表示只能執行在jdk12版本以及之後的虛擬機器中
flags: ACC_PUBLIC, ACC_SUPER

ACC_PUBLIC:這就是一個是否是public型別的訪問標誌。

ACC_SUPER: 這個falg是為了解決透過 invokespecial 指令呼叫 super 方法的問題。可以將它理解成 Java 1.0.2 的一個缺陷補丁,只有透過這樣它才能正確找到 super 類方法。從 Java 1.0.2 開始,編譯器始終會在位元組碼中生成 ACC_SUPER 訪問標識。感興趣的同學可以點選這裡來了解更多。

2.常量池

接下來,我們將要分析常量池,你也可以對照上面整體的位元組碼來理解。

#1 = Methodref          #5.#14         // java/lang/Object."<init>":()V

這是一個方法引用,這裡的#5表示索引值,然後我們可以發現索引值為5的位元組碼如下

#5 = Class              #20            // java/lang/Object

它表示這是一個Object類,同理#14指向的是一個"<init>":()V表示引用的是初始化方法。

#2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;

上面這段表示是一個欄位引用,同樣引用了#15和#16,實際上引用的就是java/lang/System類中的PrintStream物件。其他的常量池分析思路是一樣的,鑑於篇幅我就不一一說明了,只列下其中的幾個關鍵型別和資訊。

NameAndType:這個表示是名稱和型別的常量表,可以指向方法名稱或者欄位的索引,在上面的位元組碼中都是表示的實際的方法。

Utf8: 我們經常使用的是字元編碼,但是這個不是隻有字元編碼的意思,它表示一種字元編碼是Utf8的字串。它是虛擬機器中最常用的表結構,你可以理解為它可以描述方法,欄位,類等資訊。 比如:

#4 = Class              #19 #19 = Utf8               top/luozhou/test/TestJava

這裡表示#4這個索引下是一個類,然後指向的類是#19,#19是一個Utf8表,最終存放的是top/luozhou/test/TestJava,那麼這樣一連線起來就可以知道#4位置引用的類是top/luozhou/test/TestJava了。

3.構造方法資訊

接下來,我們分析下構造方法的位元組碼,我們知道,一個類初始化的時候最先執行它的構造方法,如果你沒有寫構造方法,系統會預設給你新增一個無參的構造方法。

public top.luozhou.test.TestJava();    
    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1
         0: aload_0         
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return      LineNumberTable:
        line 8: 0

descriptor: ()V :表示這是一個沒有返回值的方法。

flags: ACC_PUBLIC:是公共方法。

stack=1, locals=1, args_size=1 :表示棧中的數量為1,區域性變數表中的變數為1,呼叫引數也為1。

這裡為什麼都是1呢?這不是預設的構造方法嗎?哪來的引數?其實Java語言有一個潛規則: 在任何例項方法裡面都可以透過this來訪問到此方法所屬的物件。而這種機制的實現就是透過Java編譯器在編譯的時候作為入參傳入到方法中了,熟悉python語言的同學肯定會知道,在python中定義一個方法總會傳入一個self的引數,這也是傳入此例項的引用到方法內部,Java只是把這種機制後推到編譯階段完成而已。所以,這裡的1都是指this這個引數而已。

         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
    LineNumberTable:        line 8: 0

經過上面這個分析對於這個構造方法表達的意思也就很清晰了。

aload_0:表示把區域性變數表中的第一個變數載入到棧中,也就是this。

invokespecial:直接呼叫初始化方法。

return:呼叫完畢方法結束。

LineNumberTable:這是一個行數的表,用來記錄位元組碼的偏移量和程式碼行數的對映關係。line 8: 0表示,原始碼中第8行對應的就是偏移量0的位元組碼,因為是預設的構造方法,所以這裡並無法直觀體現出來。

另外這裡會執行Object的構造方法是因為,Object是所有類的父類,子類的構造要先構造父類的構造方法。

4.main方法資訊

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_2         1: istore_1         2: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1         6: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:        line 10: 0
        line 11: 2
        line 12: 9

有了之前構造方法的分析,我們接下來分析main方法也會熟悉很多,重複的我就略過了,這裡重點分析code部分。

stack=2, locals=2, args_size=1:這裡的棧和區域性變數表為2,引數還是為1。這是為什麼呢?因為main方法中宣告瞭一個變數a,所以區域性變數表要加一個,棧也是,所以他們是2。那為什麼args_size還是1呢?你不是說預設會把this傳入的嗎?應該是2啊。 注意:之前說的是在任何例項方法中,而這個main方法是一個靜態方法,靜態方法直接可以透過類+方法名訪問,並不需要例項物件,所以這裡就沒必要傳入了

0: iconst_2:將int型別2推送到棧頂。

1: istore_1:將棧頂int型別數值存入第二個本地變數。

2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;:獲取PrintStream類。

5: iload_1: 把第二個int型本地變數推送到棧頂。

6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V:呼叫println方法。

9: return:呼叫完畢結束方法。

這裡的LineNumberTable是有原始碼的,我們可以對照下我前面描述是否正確:


line 10: 0: 第10行表示 0: iconst_2位元組碼,這裡我們發現編譯器直接給我們計算好了把2推送到棧頂了。

line 11: 2:第11行原始碼對應的是 2: getstatic 獲取輸出的靜態類PrintStream。

line 12: 9:12行原始碼對應的是return,表示方法結束。

這裡我也畫了一個動態圖片來演示main方法執行的過程,希望能夠幫助你理解:



總結

這篇文章我從1+1的的原始碼編譯開始,分析了生成後的Java位元組碼,包括類的基本資訊,常量池,方法呼叫過程等,透過這些分析,我們對Java位元組碼有了比較基本的瞭解,也知道了Java編譯器會把最佳化手段透過編譯好的位元組碼體現出來,比如我們的1+1=2,位元組碼位元組賦值一個2給變數,而不是進行加法運算,從而最佳化了我們的程式碼,提搞了執行效率。

年前送上福利Zookeeper福利,先到先得


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946034/viewspace-2671492/,如需轉載,請註明出處,否則將追究法律責任。

相關文章