學弟學妹們,若是你想吃透 Java字節碼的話,看這篇就行了!(超級硬核,建議收藏)

同窗們好,今天二哥是來還債的,記得先拖到文末點個贊再回來細細的讀,好很差!java

最近一段時間,我一直在學習 Java 虛擬機和字節碼方面的知識,爲的就是有朝一日成爲真正牛逼的技術大佬!不知道你們有沒有這種感受,就是一開始學習編程的時候,真心不想看底層的東西,就想直接上來擼代碼,但時間久了之後,總感受缺點啥~~~~web

因而我開始閱讀《深刻理解計算機系統》、《圖解 TCP/IP》、《深刻理解 Java 虛擬機》這些偏底層的書籍,看得煩了,就去刷我以前給你們推薦過的兩個視頻課,《哈佛大學的 CS50》和《計算機科學速成課》,慢慢的,就有一種頓悟的感受,嗯,這種感受仍是挺舒服的,很容易飄的那種(嘿嘿)。面試

我以前已經分享過三篇關於 Java 虛擬機和字節碼方面的內容,你們能夠再溫習一遍。編程

class 文件
JVM 內存區域
Java 虛擬機棧數組

這三篇的內容仍是很是肝的,讀起來也比較輕鬆,但若是你是初學者,讀起來感受很吃力的話,沒關係,我再來補一篇更全面、更細緻、更通俗的,從另一個視角切入,完事了能夠把這四篇一塊兒添加到收藏夾,之後興致比較高的時候能夠再咀嚼下。併發

0一、字節碼

計算機比較「傻」,只認 0 和 1,這意味着咱們編寫的代碼最終都要編譯成機器碼才能被計算機執行。Java 在誕生之初就提出了一個很是著名的宣傳口號: 「一次編寫,到處運行」。jvm

Write Once, Run Anywhere.編程語言

爲了這個口號,Java 的親媽 Sun 公司以及其餘虛擬機提供商發佈了許多能夠在不一樣平臺上運行的 Java 虛擬機,而這些虛擬機都擁有一個共同的功能,那就是能夠載入和執行同一種與平臺無關的字節碼(Byte Code)。ide

有了 Java 虛擬機的幫助,咱們編寫的 Java 源代碼沒必要再根據不一樣平臺編譯成對應的機器碼了,只須要生成一份字節碼,而後再將字節碼文件交由運行在不一樣平臺上的 Java 虛擬機讀取後執行就能夠了。svg

現在的 Java 虛擬機很是強大,不只支持 Java 語言,還支持不少其餘的編程語言,好比說 Groovy、Scala、Koltin 等等。

來看一段代碼吧。

public class Main { 
 
  
    private int age = 18;
    public int getAge() { 
 
  
        return age;
    }
}

編譯生成 Main.class 文件後,能夠在命令行使用 xxd Main.class 打開 class 文件(我用的是 Intellij IDEA,在 macOS 環境下)。

對於這些 16 進制內容,除了開頭的 cafe babe,剩下的內容大體能夠翻譯成:啥玩意啊這…

同窗們別慌,就從"cafe babe"提及吧,這 4 個字節稱之爲魔數,也就是說,只有以"cafe babe"開頭的 class 文件才能被 Java 虛擬機接受,這 4 個字節就是字節碼文件的身份標識。

目光右移,0000 是 Java 的次版本號,0037 轉化爲十進制是 55,是主版本號,Java 的版本號從 45 開始,每升一個大版本,版本號加 1,你們能夠啓動福爾摩斯模式,推理一下。

再日後面就是字符串常量池。《class 文件》那一篇我是順着十六進制內容往下分析的,可能初學者看起來比較頭大,此次咱們換一種更容易懂的方式。

0二、反編譯字節碼文件

Java 內置了一個反編譯命令 javap,能夠經過 javap -help 瞭解 javap 的基本用法。

OK,咱們輸入命令 javap -v -p Main.class 來查看一下輸出的內容。

Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class
  Last modified 2021年4月15日; size 385 bytes
  SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c6
  Compiled from "Main.java"
public class com.itwanger.jvm.Main
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // com/itwanger/jvm/Main
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // com/itwanger/jvm/Main.age:I
   #3 = Class              #20            // com/itwanger/jvm/Main
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               age
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/itwanger/jvm/Main;
  #14 = Utf8               getAge
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               Main.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // age:I
  #20 = Utf8               com/itwanger/jvm/Main
  #21 = Utf8               java/lang/Object
{
  private int age;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  public com.itwanger.jvm.Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        18
         7: putfield      #2                  // Field age:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/itwanger/jvm/Main;

  public int getAge();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/itwanger/jvm/Main;
}
SourceFile: "Main.java"

睜大眼睛瞧過去,感受內容挺多的。同窗們不要着急,咱們來一行一行分析。

第 1 行:

Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class

字節碼文件的位置。

第 2 行:

Last modified 2021年4月15日; size 385 bytes

字節碼文件的修改日期、文件大小。

第 3 行:

SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c

字節碼文件的 SHA-256 值。

第 4 行:

Compiled from "Main.java"

說明該字節碼文件編譯自 Main.java 源文件。

第 5 行:

public class com.itwanger.jvm.Main

字節碼文件的類全名。

第 6 行 minor version: 0,次版本號。

第 7 行 major version: 55,主版本號。

第 8 行:

flags: (0x0021) ACC_PUBLIC, ACC_SUPER

類訪問標記,一共有 8 種。

代表當前類是 ACC_PUBLIC | ACC_SUPER。位運算符 | 的意思是若是相對應位是 0,則結果爲 0,不然爲 1,因此 0x0001 | 0x0020 的結果是 0x0021(須要轉成二進制進行運算)。

第 9 行:

this_class: #3                          // com/itwanger/jvm/Main

當前類的索引,指向常量池中下標爲 3 的常量,能夠看得出當前類是 Main 類。

第 10 行:

super_class: #4                         // java/lang/Object

父類的索引,指向常量池中下標爲 6 的常量,能夠看得出當前類的父類是 Object 類。

第 11 行:

interfaces: 0, fields: 1, methods: 2, attributes: 1

當前類有 0 個接口,1 個字段(age),2 個方法(write()方法和缺省的默認構造方法),1 個屬性(該類僅有的一個屬性是 SourceFIle,包含了源碼文件的信息)。

0三、常量池

接下來是 Constant pool,也就是字節碼文件最重要的常量池部分。能夠把常量池理解爲字節碼文件中的資源倉庫,主要存放兩大類信息。

1)字面量(Literal),有點相似 Java 中的常量概念,好比文本字符串,final 常量等。

2)符號引用(Symbolic References),屬於編譯原理方面的概念,包括 3 種:

  • 類和接口的全限定名(Fully Qualified Name)
  • 字段的名稱和描述符(Descriptor)
  • 方法的名稱和描述符

Java 虛擬機是在加載字節碼文件的時候才進行的動態連接,也就是說,字段和方法的符號引用只有通過運行期轉換後才能得到真正的內存地址。當 Java 虛擬機運行時,須要從常量池獲取對應的符號引用,而後在類建立或者運行時解析並翻譯到具體的內存地址上。

當前字節碼文件中一共有 21 個常量,它們之間是有連接的,逐個分析會比較亂,咱們採用順藤摸瓜的方式,從上依次往下看,那些被連接的常量咱們就點到爲止。

  • # 號後面跟的是索引,索引沒有從 0 開始而是從 1 開始,是由於設計者考慮到,「若是要表達不引用任何一個常量的含義時,能夠將索引值設爲 0 來表示」(《深刻理解 Java 虛擬機》描述的)。

  • = 號後面跟的是常量的類型,沒有包含前綴 CONSTANT_ 和後綴 _info

  • 全文中提到的索引等同於下標,爲了靈活描述,沒有作統一。


第 1 個常量:

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

類型爲 Methodref,代表是用來定義方法的,指向常量池中下標爲 4 和 18 的常量。

第 4 個常量:

#4 = Class              #21            // java/lang/Object

類型爲 Class,代表是用來定義類(或者接口)的,指向常量池中下標爲 21 的常量。

第 21 個常量:

#21 = Utf8               java/lang/Object

類型爲 Utf8,UTF-8 編碼的字符串,值爲 java/lang/Object

第 18 個常量:

#18 = NameAndType        #7:#8          // "<init>":()V

類型爲 NameAndType,代表是字段或者方法的部分符號引用,指向常量池中下標爲 7 和 8 的常量。

第 7 個常量:

#7 = Utf8               <init>

類型爲 Utf8,UTF-8 編碼的字符串,值爲 <init>,代表爲構造方法。

第 8 個常量:

#8 = Utf8               ()V

類型爲 Utf8,UTF-8 編碼的字符串,值爲 ()V,代表方法的返回值爲 void。

到此爲止,第 1 個常量算是摸完了。組合起來的意思就是,Main 類使用的是默認的構造方法,來源於 Object 類。


第 2 個常量:

#2 = Fieldref           #3.#19         // com/itwanger/jvm/Main.age:I

類型爲 Fieldref,代表是用來定義字段的,指向常量池中下標爲 3 和 19 的常量。

第 3 個常量:

#3 = Class              #20            // com/itwanger/jvm/Main

類型爲 Class,代表是用來定義類(或者接口)的,指向常量池中下標爲 20 的常量。

第 19 個常量:

#19 = NameAndType        #5:#6          // age:I

類型爲 NameAndType,代表是字段或者方法的部分符號引用,指向常量池中下標爲 5 和 6 的常量。

第 5 個常量:

#5 = Utf8               age

類型爲 Utf8,UTF-8 編碼的字符串,值爲 age,代表字段名爲 age。

第 6 個常量:

#6 = Utf8               I

類型爲 Utf8,UTF-8 編碼的字符串,值爲 I,代表字段的類型爲 int。

關於字段類型的描述符映射表以下圖所示。

到此爲止,第 2 個常量算是摸完了。組合起來的意思就是,聲明瞭一個類型爲 int 的字段 age。


0四、字段表集合

字段表用來描述接口或者類中聲明的變量,包括類變量和成員變量,但不包含聲明在方法中局部變量。

字段的修飾符通常有:

  • 訪問權限修飾符,好比 public private protected
  • 靜態變量修飾符,好比 static
  • final 修飾符
  • 併發可見性修飾符,好比 volatile
  • 序列化修飾符,好比 transient

而後是字段的類型(能夠是基本數據類型、數組和對象)和名稱。

在 Main.class 字節碼文件中,字段表的信息以下所示。

private int age;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

代表字段的訪問權限修飾符爲 private,類型爲 int,名稱爲 age。

字段的訪問標誌和類的訪問標誌很是相似。

0五、方法表集合

方法表用來描述接口或者類中聲明的方法,包括類方法和成員方法,以及構造方法。方法的修飾符和字段略有不一樣,好比說 volatile 和 transient 不能用來修飾方法,再好比說方法的修飾符多了 synchronized、native、strictfp 和 abstract。

下面這部分爲構造方法,返回類型爲 void,訪問標誌爲 public。

public com.itwanger.jvm.Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC

來詳細看一下其中 Code 屬性。

Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        18
         7: putfield      #2                  // Field age:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/itwanger/jvm/Main;
  • stack 爲最大操做數棧,Java 虛擬機在運行的時候會根據這個值來分配棧幀的操做數棧深度。

  • locals 爲局部變量所須要的存儲空間,單位爲槽(slot),方法的參數變量和方法內的局部變量都會存儲在局部變量表中。

  • args_size 爲方法的參數個數。

爲何 stack 的值爲 2,locals 的值爲 1,args_size 的值爲 1 呢? 默認的構造方法不是沒有參數和局部變量嗎?

這是由於有一個隱藏的 this 變量,只要不是靜態方法,都會有一個當前類的對象 this 悄悄的存在着。這就解釋了爲何 locals 和 args_size 的值爲 1 的問題。那爲何 stack 的值爲 2 呢?由於字節碼指令 invokespecial(調用父類的構造方法進行初始化)會消耗掉一個當前類的引用,因此 aload_0 執行了 2 次,也就意味着操做數棧的大小爲 2。

關於字節碼指令,咱們後面再詳細介紹。

  • LineNumberTable,該屬性的做用是描述源碼行號與字節碼行號(字節碼偏移量)之間的對應關係。

  • LocalVariableTable,該屬性的做用是描述幀棧中的局部變量與源碼中定義的變量之間的關係。你們仔細看一下,就能看到 this 的影子了。

下面這部分爲成員方法 getAge(),返回類型爲 int,訪問標誌爲 public。

public int getAge();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC

理解了構造方法的 Code 屬性後,再看 getAge() 方法的 Code 屬性時,就很容易理解了。

Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/itwanger/jvm/Main;

最大操做數棧爲 1,局部變量所須要的存儲空間爲 1,方法的參數個數爲 1,是由於局部變量只有一個隱藏的 this,而且字節碼指令中只執行了一次 aload_0。


原本想着這一篇就完全把 Java 字節碼給結束掉,沒想到還得再學習一下字節碼指令,難頂!

其實學習就是這樣,能夠橫向擴展,也能夠縱向擴展。當咱們初學編程的時候,特別想多學一點,屬於橫向擴展,當有了必定的編程經驗後,想更上一層樓,就須要縱向擴展,不斷深刻地學,連根拔起,從而造成本身的知識體系。

不管是從十六進制的字節碼角度,仍是 jclasslib 圖形化查看反編譯後的字節碼的角度,也或者是今天這樣從 javap 反編譯後的角度,都能窺探出一些新的內容來!

初學者一開始接觸字節碼的時候會感受比較頭大,不要緊,我當初也是這樣,隨着時間的推移,經驗的積累,慢慢就行了,越往深處鑽,就越能體會到那種「技術我有,雄霸天下」的感受~

說兩句,立刻秋招了,能夠開始準備了。

必定記得刷一刷面試題,背一背八股文,要乖哦,千萬不要抗拒!千萬不要裸面,真的!其實私下裏,不少學弟學妹們都向我哭訴過,說大廠的面試題太難了,有的題出的真的是萬萬沒想到啊(狗頭)。甚至有些中小廠的面試題都很難對答如流(他們的面試官可能看過我這份面試題庫,哈哈哈),有了這份面試題庫後,你們不再用慌了!

V4.0 《JavaGuide 面試突擊版》來啦!GitHub 上標星 98.1k,幫你成功上岸!

我是一直在悄悄打怪的二哥,但願能和同窗們一塊兒,變得更強,更禿(不不不,更帥),既然看到着了,就賞個三連吧,只收藏也不是不能夠!

下期見~